# atom.io

Source: docs/source/pages/docs/index.mdx
URL: /docs/atom-io

# atom.io

## core module

`atom.io` is the core state module: it gives you primitives to declare reactive state,
derive more state from it, subscribe to changes, and operate on a store directly from
TypeScript.

Start with `atom`, `selector`, `getState`, and `setState`. Add families when you need many
similar states, transactions when updates need coordination, and timelines when you want
history.

## package contents

<table-wrapper>

| Export | Description |
| --- | --- |
| `atom` | Declare a reactive variable. |
| `mutableAtom` | Declare a reactive variable backed by a mutable, trackable data structure. |
| `selector` | Declare a reactive variable derived from other reactive variables. |
| `atomFamily` | Compose a function that can create reactive variables of a single type dynamically. |
| `selectorFamily` | Compose a function that can create reactive variables derived from other reactive variables dynamically. |
| `transaction` | Declare a function that can batch multiple atom changes into a single update. |
| `timeline` | Track the history of a group of reactive variables. |
| `timelineFamily` | Partition atom families into independently addressable histories. |
| `join` | Manage bidirectional relations between two sets of keys. |
| `subscribe` | Subscribe to a reactive variable, calling a callback whenever it is updated. |
| `getState` | Get the value of a reactive variable. If the reactive variable is a selector, the value is derived from other reactive variables. |
| `setState` | Set the value of an atom or a writable selector. If the target is a writable selector, atom.io calls the selector's `set` callback so it can update the dependencies that determine the selector's next value. |
| `resetState` | Reset a writable state to its default value. |
| `disposeState` | Dispose of an allocated family member and run its cleanup. |
| `Silo` | An isolated store with all of the above functions bound to it. Use one for each independently instantiated application graph, request, preview, test, or sandbox. |

</table-wrapper>

## atom

### declare an atom
Source: docs/source/exhibits/core/atom/declare-an-atom.ts

```ts
import { atom } from "atom.io"

export const countAtom = atom<number>({
	key: `count`,
	default: 0,
})
```

Imagine an `atom` as a "reactive variable," with a key, a type, and a default
value.

### an atom token is a reference
Source: docs/source/exhibits/core/atom/an-atom-token-is-a-reference.ts

```ts
import { getState } from "atom.io"

import { countAtom } from "./declare-an-atom.ts"

countAtom // -> { key: `count`, type: `atom` }
getState(countAtom) // -> 0
getState({ key: `count`, type: `atom` }) // -> 0
```

As you can see, what is returned from `atom` does not contain the value itself.

Instead, it returns an importable, serializable, and replaceable reference to the value.

We call this an `AtomToken`. In this case, an `AtomToken<number>`.

Tokens are serializable references. Every token has a `key` and a `type` field.
The `type` field is the public discriminator for the kind of resource the token
references, such as `atom`, `atom_family`, `timeline`, or `transaction`.

### get and set an atom
Source: docs/source/exhibits/core/atom/get-and-set-an-atom.ts

```ts
import { getState, setState } from "atom.io"

import { countAtom } from "./declare-an-atom.ts"

getState(countAtom) // -> 0
setState(countAtom, 1)
getState(countAtom) // -> 1

// @ts-expect-error `hello` is not a number
setState(countAtom, `hello`)
```

An atom's value is accessed by calling `getState` and `setState` with the atom's
token.

TypeScript will discourage you from setting the wrong type of value.

### subscribe to an atom
Source: docs/source/exhibits/core/atom/subscribe-to-an-atom.ts

```ts
import { subscribe } from "atom.io"

import { countAtom } from "./declare-an-atom.ts"

subscribe(countAtom, (count) => {
	console.log(`count is now ${count.newValue}`)
})
```

Unlike a standard variable, you can `subscribe` to an atom. The callback you
pass to the subscription will be called whenever the atom is set to a new value.

### subscribe is the foundation of reactivity
Source: docs/source/exhibits/core/atom/subscribe-is-the-foundation-of-reactivity.tsx

```tsx
import { useO } from "atom.io/react"

import { countAtom } from "./declare-an-atom.ts"

function Component() {
	const count = useO(countAtom)
	return <>{count}</>
}
```

This is an example of the **observer pattern**. Following the observer pattern
allows atom.io to integrate cleanly with an observer like React. More on this
later.

## selector

### declare a selector
Source: docs/source/exhibits/core/selector/declare-a-selector.ts

```ts
import { atom, selector } from "atom.io"

export const dividendAtom = atom<number>({
	key: `dividend`,
	default: 0,
})

export const divisorAtom = atom<number>({
	key: `divisor`,
	default: 2,
})

export const quotientSelector = selector<number>({
	key: `quotient`,
	get: ({ get }) => {
		const dividend = get(dividendAtom)
		const divisor = get(divisorAtom)
		return dividend / divisor
	},
})
```

A selector is also a reactive variable, but its value is derived from other atoms or selectors.

### use a selector
Source: docs/source/exhibits/core/selector/use-a-selector.ts

```ts
import { getState, setState } from "atom.io"

import {
	dividendAtom,
	divisorAtom,
	quotientSelector,
} from "./declare-a-selector.ts"

getState(dividendAtom) // -> 0
getState(divisorAtom) // -> 2
getState(quotientSelector) // -> 0

setState(dividendAtom, 4)

getState(quotientSelector) // -> 2
```

In this example, we can see that by setting `dividendState` to a new value, the value of `quotientState` is automatically updated.

## families

Sometimes you need a lot of the same type of atom or selector. The `atomFamily` and `selectorFamily` functions provide a convenient interface for declaring states dynamically.

### declare a family
Source: docs/source/exhibits/core/families/declare-a-family.tsx

```tsx
import { atomFamily, getState } from "atom.io"
import { useO } from "atom.io/react"
import * as React from "react"

type PointXY = { x: number; y: number }

export const pointAtoms = atomFamily<PointXY, string>({
	key: `point`,
	default: { x: 0, y: 0 },
})

getState(pointAtoms, `example`) // -> { x: 0, y: 0 }

export function Point(props: { pointId: string }): React.JSX.Element {
	const { x, y } = useO(pointAtoms, props.pointId)

	return <div className="point" style={{ left: x, top: y }} />
}
```

For example, imagine an editor containing many points. An `atomFamily` can give each
point its own keyed state while keeping one shared declaration.

Choose state boundaries according to how values are observed and updated. A point's x
and y coordinates usually form one coherent value: components read them together, and
dragging replaces them together. Separate coordinate families may be useful when the
axes genuinely change or are observed independently, but finer granularity also
introduces more writes, subscriptions, and coordination.

Aim for the smallest coherent unit of state, not the smallest representable value.
Granularity can prevent unrelated updates, but it is not automatically faster. Let
application behavior determine the model, and profile before splitting state solely for
performance.

Keep explicit indexes for the family members you need to iterate. When several
independent states must change as one operation, update them in a transaction.

### use an index to track family members
Source: docs/source/exhibits/core/families/use-an-index-to-track-family-members.tsx

```tsx
import { atom } from "atom.io"
import { useO } from "atom.io/react"

import { Point } from "./declare-a-family.tsx"

export const pointKeysAtom = atom<string[]>({
	key: `pointKeys`,
	default: [],
})

function AllPoints() {
	const pointIds = useO(pointKeysAtom)
	return (
		<>
			{pointIds.map((pointId) => {
				return <Point key={pointId} pointId={pointId} />
			})}
		</>
	)
}
```

In this example, we use a single `atom<string[]>` to track the members of the family.

It is up to you to decide how to track the members of the families you create. `atom.io` does not do this for you, because different kinds of collections have different performance characteristics. There is no one-size-fits-all solution.

## reset and dispose

Use `resetState` when a state should keep existing, but its value should return
to its default. For an atom, that writes the default value again. If the default
is a function, atom.io runs it again. For a mutable atom, reset creates a fresh
transceiver. For a writable selector, atom.io resets the root atoms that
determine the selector's value, then recomputes the selector.

Reset is especially important for `Loadable` state backed by remote data. Once
an async default has resolved or errored, reset is the direct way to discard the
retained result and start that load again. After a mutation, resetting the
smallest affected loadable state makes the remote system the source of truth
again. This matters most for data that only the remote can assign or normalize,
such as database ids, timestamps, counts, permission-derived fields, or
canonical row shapes. For more patterns around fetched and RPC-backed state, see
the [remote data guide](/docs/remote-data).

Use `disposeState` when a dynamically allocated family member has left your
model. Disposal removes that member from the store, clears its retained
state data, and runs any cleanup returned by atom effects. Pass either a family
and key, or a family member token returned by `findState`.

### reset and dispose
Source: docs/source/exhibits/core/families/reset-and-dispose.ts

```ts
import {
	atomFamily,
	disposeState,
	getState,
	resetState,
	setState,
} from "atom.io"
import { stateExists } from "atom.io/testing"

const rowHeightAtoms = atomFamily<number, string>({
	key: `rowHeight`,
	default: 32,
})

setState(rowHeightAtoms, `header`, 48)
getState(rowHeightAtoms, `header`) // -> 48

resetState(rowHeightAtoms, `header`)
getState(rowHeightAtoms, `header`) // -> 32
stateExists(rowHeightAtoms, `header`) // -> true

disposeState(rowHeightAtoms, `header`)
stateExists(rowHeightAtoms, `header`) // -> false
```

`disposeState` only disposes family members. Standalone atoms and selectors are
permanent parts of the store shape, so attempting to dispose one logs an error
and leaves it in place. Disposal also does not maintain your application-level
indexes for you, and it does not automatically prune downstream selector family
members that have already been created.

## transaction

Transactions allow you to batch multiple atom changes into a single update. This is useful for validating a complex set of changes before it is applied to the store.

The `do` callback receives a toolkit bound to the transaction's temporary store.
Reads see changes already made in the transaction. Writes, resets, disposals,
and nested transactions are applied to the real store only if the transaction
finishes without throwing.

After a successful transaction, ordinary atom and selector subscribers run only
after all ordinary atom values have settled. Subscriber callbacks can read other
atoms changed by the transaction without seeing a partially committed snapshot,
and selectors affected by several writes recompute once from the settled state.

<table-wrapper>

| Export | Description |
| --- | --- |
| `get` | Read the value of an atom or selector. |
| `set` | Write the value of an atom or writable selector. |
| `reset` | Reset a writable state while keeping it in the store. |
| `find` | Get a token for a family member without reading its value. |
| `json` | Get the writable JSON token for a mutable atom. |
| `dispose` | Dispose of an allocated family member. |
| `run` | Run another transaction inside this transaction. |
| `env` | Read environment data for the current store. |
| `relations` | Work with `join` relations inside the transaction. Use `find` for relation selectors, `edit` to mutate relations, and `internal` only when advanced integrations need the underlying relation atom families. |

</table-wrapper>

### use a family in a transaction
Source: docs/source/exhibits/core/transaction/use-a-family-in-a-transaction.ts

```ts
import { atom, atomFamily, transaction } from "atom.io"

export type PublicUser = {
	id: string
	displayName: string
}

export const publicUserAtoms = atomFamily<PublicUser, string>({
	key: `publicUser`,
	default: (id) => ({ id, displayName: `` }),
})

export const userKeysAtom = atom<string[]>({
	key: `userKeys`,
	default: [],
})

export const addUserTransaction = transaction<(user: PublicUser) => void>({
	key: `addUser`,
	do: ({ get, set }, user) => {
		set(publicUserAtoms, user.id, user)
		if (!get(userKeysAtom).includes(user.id)) {
			set(userKeysAtom, (current) => [...current, user.id])
		}
	},
})
```

A common use case is creating some new state using a family and adding it to an index tracking members of that family.

### iterate through an index changing the value of some atoms
Source: docs/source/exhibits/core/transaction/iterate-through-an-index-changing-the-value-of-some-atoms.ts

```ts
import { atom, atomFamily, selectorFamily, transaction } from "atom.io"

export const nowAtom = atom<number>({
	key: `now`,
	default: Date.now(),
	effects: [
		({ setSelf }) => {
			const interval = setInterval(() => {
				setSelf(Date.now())
			}, 1000)
			return () => {
				clearInterval(interval)
			}
		},
	],
})

export const timerKeysAtom = atom<string[]>({
	key: `timerKeys`,
	default: [],
})

export const timerStartedAtoms = atomFamily<number, string>({
	key: `timerStarted`,
	default: 0,
})
export const timerLengthAtoms = atomFamily<number, string>({
	key: `timerLength`,
	default: 60_000,
})
const timerRemainingSelectors = selectorFamily<number, string>({
	key: `timerRemaining`,
	get:
		(id) =>
		({ get }) => {
			const now = get(nowAtom)
			const started = get(timerStartedAtoms, id)
			const length = get(timerLengthAtoms, id)
			return Math.max(0, length - (now - started))
		},
})

export const addOneMinuteToAllRunningTimersTransaction = transaction({
	key: `addOneMinuteToAllRunningTimers`,
	do: ({ get, set }) => {
		const timerIds = get(timerKeysAtom)
		for (const timerId of timerIds) {
			if (get(timerRemainingSelectors, timerId) > 0) {
				set(timerLengthAtoms, timerId, (current) => current + 60_000)
			}
		}
	},
})
```

In this example, we add a minute to all running timers.

### dispose a family member in a transaction
Source: docs/source/exhibits/core/transaction/dispose-a-family-member-in-a-transaction.ts

```ts
import { atom, atomFamily, transaction } from "atom.io"

type Draft = {
	id: string
	title: string
}

export const draftAtoms = atomFamily<Draft, string>({
	key: `draft`,
	default: (id) => ({ id, title: `` }),
})

export const draftKeysAtom = atom<string[]>({
	key: `draftKeys`,
	default: [],
})

export const deleteDraftTransaction = transaction<(draftId: string) => void>({
	key: `deleteDraft`,
	do: ({ dispose, get, set }, draftId) => {
		const draftIds = get(draftKeysAtom)
		if (!draftIds.includes(draftId)) return

		set(
			draftKeysAtom,
			draftIds.filter((id) => id !== draftId),
		)
		dispose(draftAtoms, draftId)
	},
})
```

When you remove family state, update your application-level index and dispose
the family member in the same transaction.

### try catch a failed transaction
Source: docs/source/exhibits/core/transaction/try-catch-a-failed-transaction.ts

```ts
import type { Loadable } from "atom.io"
import { atom, atomFamily, runTransaction, transaction } from "atom.io"

export type GameItems = { coins: number }
export type Inventory = Partial<Readonly<GameItems>>

export const myIdAtom = atom<Loadable<string>>({
	key: `myId`,
	default: async () => {
		const response = await fetch(`https://io.fyi/api/myId`)
		const { id } = await response.json()
		return id
	},
})

export const playerInventoryAtoms = atomFamily<Inventory, string>({
	key: `playerInventory`,
	default: {},
})

export const giveCoinsTransaction = transaction<
	(playerId: string, amount: number) => Promise<void>
>({
	key: `giveCoins`,
	do: async ({ get, set }, playerId, amount) => {
		const myId = await get(myIdAtom)
		const myInventory = get(playerInventoryAtoms, myId)
		if (myInventory.coins === undefined) {
			throw new Error(`Your inventory is missing coins`)
		}
		const myCoins = myInventory.coins
		if (myCoins < amount) {
			throw new Error(`You don't have enough coins`)
		}
		const theirInventory = get(playerInventoryAtoms, playerId)
		const theirCoins = theirInventory.coins ?? 0
		set(playerInventoryAtoms, myId, (previous) => ({
			...previous,
			coins: myCoins - amount,
		}))
		set(playerInventoryAtoms, playerId, (previous) => ({
			...previous,
			coins: theirCoins + amount,
		}))
	},
})
;async () => {
	try {
		await runTransaction(giveCoinsTransaction)(`playerId`, 3)
	} catch (thrown) {
		if (thrown instanceof Error) {
			alert(thrown.message)
		}
	}
}
```

If a transaction throws, the state of the store is not changed. However, it is up to you to handle the error.

## timeline

Timelines allow you to track the history of a group of atoms. If these atoms are set, or set as a group by a selector or transaction, the timeline will record the changes. A timeline can be used to undo and redo changes.

### create a timeline
Source: docs/source/exhibits/core/timeline/create-a-timeline.ts

```ts
import { timeline } from "atom.io"

import { pointAtoms } from "../families/declare-a-family.tsx"

export const coordinatesTimeline = timeline({
	key: `coordinates`,
	scope: [pointAtoms],
})
```

For long-lived editors, add a timeline effect that collects old undo history.
`onRecord` observes each complete logical update before it settles, while
exposing that update as deeply readonly data. This lets effects inspect records
without accidentally changing the timeline's stored event.
`cullUndoSteps` safely removes the oldest complete checkpoints. Selector writes and
transactions are never split.

### retain bounded history
Source: docs/source/exhibits/core/timeline/retain-bounded-history.ts

```ts
import type { TimelineEffect } from "atom.io"
import { atom, timeline } from "atom.io"

export const documentAtom = atom<string>({
	key: `document`,
	default: ``,
})

export const keepLatest100Steps: TimelineEffect = ({
	cullUndoSteps,
	onRecord,
}) => {
	onRecord(() => {
		cullUndoSteps(100)
	})
}

export const documentTimeline = timeline({
	key: `document`,
	scope: [documentAtom],
	effects: [keepLatest100Steps],
})
```

Without an effect, undo history is unlimited. Effects can also call
`cullUndoSteps` at arbitrary times, subscribe to application-owned policy signals,
and return cleanup for disposal. An arbitrary cull that removes history publishes
a `timeline_cull` event with logical undo-step counts, while culling during
`onRecord` remains part of that record's atomic update. Timeline-family effect
factories create an independent effect lifecycle for every member.

In this example, we create a timeline that tracks the history of a family of point atoms.

### subscribe to a timeline
Source: docs/source/exhibits/core/timeline/subscribe-to-a-timeline.ts

```ts
import { setState, subscribe } from "atom.io"

import { pointAtoms } from "../families/declare-a-family.tsx"
import { coordinatesTimeline } from "./create-a-timeline.ts"

subscribe(coordinatesTimeline, (value) => {
	console.log(value)
})

setState(pointAtoms, `sample_key`, { x: 1, y: 0 })
/* {
  newValue: { x: 1, y: 0 },
  oldValue: { x: 0, y: 0 },
  key: `sample_key`,
  type: `atom_update`,
  timestamp: 1629780000000,
  family: {
    key: `point`,
    type: `atom_family`,
  }
} */
```

In this example, we subscribe to the timeline. The logged update is an `atom_update`:
it includes `oldValue`, `newValue`, the family member `key`, a `timestamp`, and the
family token for the state that changed.

### undo and redo changes
Source: docs/source/exhibits/core/timeline/undo-and-redo-changes.ts

```ts
import { getState, redo, setState, subscribe, undo } from "atom.io"

import { pointAtoms } from "../families/declare-a-family.tsx"
import { coordinatesTimeline } from "./create-a-timeline.ts"

subscribe(coordinatesTimeline, (value) => {
	console.log(value)
})

setState(pointAtoms, `sample_key`, { x: 1, y: 0 })
getState(pointAtoms, `sample_key`) // { x: 1, y: 0 }
setState(pointAtoms, `sample_key`, { x: 2, y: 0 })
getState(pointAtoms, `sample_key`) // { x: 2, y: 0 }
undo(coordinatesTimeline)
getState(pointAtoms, `sample_key`) // { x: 1, y: 0 }
redo(coordinatesTimeline)
getState(pointAtoms, `sample_key`) // { x: 2, y: 0 }
```

In this example, we undo and redo changes to the timeline.

Use `inspectTimeline` when you need to read the timeline cursor directly. It returns
the current position as `at`, and the number of retained updates as `length`.

### inspect a timeline
Source: docs/source/exhibits/core/timeline/inspect-a-timeline.ts

```ts
import { inspectTimeline, setState, undo } from "atom.io"

import { pointAtoms } from "../families/declare-a-family.tsx"
import { coordinatesTimeline } from "./create-a-timeline.ts"

inspectTimeline(coordinatesTimeline) // -> { at: 0, length: 0 }

setState(pointAtoms, `sample_key`, { x: 1, y: 0 })
setState(pointAtoms, `sample_key`, { x: 2, y: 0 })

inspectTimeline(coordinatesTimeline) // -> { at: 2, length: 2 }

undo(coordinatesTimeline)

inspectTimeline(coordinatesTimeline) // -> { at: 1, length: 2 }
```

### timeline families

Use a timeline family when one dynamic group of atoms needs a separate history for
each application key. Each `scopeFamily` extractor receives an atom-family member's
canonical key and returns the timeline key that owns it. Return `undefined` to leave
a member untracked.

### create a timeline family
Source: docs/source/exhibits/core/timeline/create-a-timeline-family.ts

```ts
import {
	atomFamily,
	disposeTimeline,
	findTimeline,
	inspectTimeline,
	scopeFamily,
	setState,
	timelineFamily,
} from "atom.io"

type PointKey = readonly [glyphId: string, pointId: string]

export const glyphNameAtoms = atomFamily<string, string>({
	key: `glyphName`,
	default: `Untitled glyph`,
})

export const pointXAtoms = atomFamily<number, PointKey>({
	key: `pointX`,
	default: 0,
})

export const glyphTimelines = timelineFamily<string>({
	key: `glyph`,
	scope: [
		scopeFamily(glyphNameAtoms, {
			timelineKey: (glyphId) => glyphId,
		}),
		scopeFamily(pointXAtoms, {
			timelineKey: ([glyphId]) => (glyphId === `preview` ? undefined : glyphId),
		}),
	],
})

export function editPoint(glyphId: string, pointId: string, x: number): void {
	const glyphTimeline = findTimeline(glyphTimelines, glyphId)
	setState(pointXAtoms, [glyphId, pointId], x)

	inspectTimeline(glyphTimeline) // -> { at: 1, length: 1 }
}

export function closeGlyph(glyphId: string): void {
	disposeTimeline(glyphTimelines, glyphId)
}
```

`findTimeline` lazily creates and caches a member. Existing matching atoms attach
without retroactively recording their creation, and later creations, updates,
transactions, selector writes, and atom disposal are routed to that same member.
The family overloads of `inspectTimeline`, `subscribe`, `undo`, `redo`, and
`clearTimeline` perform the same lazy lookup.

`disposeTimeline` is the exception: its family overload does not create a missing
member. Disposing removes the member's subscriptions and retained history. A later
lookup creates a fresh timeline and attaches the currently live matching atoms.

A `Silo` exposes bound versions of the same declaration, lookup, control, and
disposal APIs, so timeline-family work stays inside the Silo's store.

### use a timeline family in a silo
Source: docs/source/exhibits/core/timeline/use-a-timeline-family-in-a-silo.ts

```ts
import { scopeFamily, Silo } from "atom.io"

export const documentSilo = new Silo({
	name: `document`,
	lifespan: `ephemeral`,
	isProduction: false,
})

export const glyphNameAtoms = documentSilo.atomFamily<string, string>({
	key: `glyphName`,
	default: `Untitled glyph`,
})

export const glyphTimelines = documentSilo.timelineFamily<string>({
	key: `glyph`,
	scope: [
		scopeFamily(glyphNameAtoms, {
			timelineKey: (glyphId) => glyphId,
		}),
	],
})

export function undoGlyph(glyphId: string): void {
	documentSilo.undo(glyphTimelines, glyphId)
}

export function closeGlyph(glyphId: string): void {
	documentSilo.disposeTimeline(glyphTimelines, glyphId)
}
```

Transactions recorded by several timelines retain one shared operation identity.
Use `undo` when only one timeline should move. Use `undoTransaction` and
`redoTransaction` to move an instance wherever it is at the relevant timeline head.
Timelines that have moved elsewhere are left unchanged.

A transaction applies atomically when it runs. However, if its atoms belong to
multiple timelines, those timelines can later move independently and split the
transaction's effects. If the effects must remain atomic over time, do not update
atoms from multiple timelines in one transaction. Keep that state in one timeline.

### undo a transaction across timelines
Source: docs/source/exhibits/core/timeline/undo-a-transaction-across-timelines.ts

```ts
import {
	findTimeline,
	redoTransaction,
	runTransaction,
	transaction,
	undo,
	undoTransaction,
} from "atom.io"

import { glyphTimelines, pointXAtoms } from "./create-a-timeline-family.ts"

export const addExtremaTransaction = transaction<
	(glyphIds: readonly string[]) => void
>({
	key: `addExtrema`,
	do: ({ set }, glyphIds) => {
		for (const glyphId of glyphIds) {
			set(pointXAtoms, [glyphId, `top-extremum`], 100)
		}
	},
})

export function addExtrema(glyphIds: readonly string[]): void {
	for (const glyphId of glyphIds) {
		findTimeline(glyphTimelines, glyphId)
	}
	runTransaction(addExtremaTransaction)(glyphIds)
}

export function undoOneGlyph(glyphId: string): void {
	undo(glyphTimelines, glyphId)
}

export function undoAddExtrema(): void {
	undoTransaction(addExtremaTransaction)
}

export function redoAddExtrema(): void {
	redoTransaction(addExtremaTransaction)
}
```

React and Solid both accept a timeline family and key in `useTL`. Their hooks resolve
the member in the current provider store and switch subscriptions when the key
changes. When a transaction is at that member's current undo or redo head, the hook
also exposes the corresponding coordinated transaction control.

### React
Source: docs/source/exhibits/react/use-timeline-family.tsx

```tsx
import { useTL } from "atom.io/react"

import { glyphTimelines } from "../core/timeline/create-a-timeline-family.ts"

export function GlyphHistory(props: { glyphId: string }): React.JSX.Element {
	const { at, length, undo, redo, clear } = useTL(glyphTimelines, props.glyphId)

	return (
		<>
			<span>
				{at} / {length}
			</span>
			<button type="button" disabled={at === 0} onClick={undo}>
				undo
			</button>
			<button type="button" disabled={at === length} onClick={redo}>
				redo
			</button>
			<button type="button" onClick={clear}>
				clear history
			</button>
		</>
	)
}
```

### Solid
Source: docs/source/exhibits/solid/use-timeline-family.tsx

```tsx
/** @jsxImportSource solid-js */
import { useTL } from "atom.io/solid"
import type { JSX } from "solid-js"

import { glyphTimelines } from "../core/timeline/create-a-timeline-family.ts"

export function GlyphHistory(props: { glyphId: string }): JSX.Element {
	const history = useTL(glyphTimelines, props.glyphId)

	return (
		<>
			<span>
				{history().at} / {history().length}
			</span>
			<button type="button" onClick={history().undo}>
				undo
			</button>
			<button type="button" onClick={history().redo}>
				redo
			</button>
		</>
	)
}
```

## join

Use `join` when a relationship needs to be its own state.

You will not always need this. But for apps that need fluid, frontend-reconfigurable relations, `join` is a powerful fit.

For example, imagine a playlist editor. Tracks are their own entities, and playlists are their own entities. A track can appear in many playlists, and a playlist can contain many tracks.

The tracks do not belong to the playlist object, and the playlists do not belong to the track object. The relationship is its own state, so the UI can reshape it freely.

### declare playlist tracks
Source: docs/source/exhibits/core/advanced/join/declare-playlist-tracks.ts

```ts
import { join } from "atom.io"

type PlaylistKey = `playlist::${string}`
type TrackKey = `track::${string}`

export const playlistTracks = join({
	key: `playlistTracks`,
	between: [`playlist`, `track`],
	cardinality: `n:n`,
	isAType: (input): input is PlaylistKey => input.startsWith(`playlist::`),
	isBType: (input): input is TrackKey => input.startsWith(`track::`),
})
```

You can read the relation from either side.

### find tracks in playlist
Source: docs/source/exhibits/core/advanced/join/find-tracks-in-playlist.ts

```ts
import { findRelations } from "atom.io"

import { playlistTracks } from "./declare-playlist-tracks.ts"

const tracksInRoadTripState = findRelations(
	playlistTracks,
	`playlist::road-trip`,
).trackKeysOfPlaylist
```

### find playlists for track
Source: docs/source/exhibits/core/advanced/join/find-playlists-for-track.ts

```ts
import { findRelations } from "atom.io"

import { playlistTracks } from "./declare-playlist-tracks.ts"

const playlistsUsingDreamsState = findRelations(
	playlistTracks,
	`track::dreams`,
).playlistKeysOfTrack
```

Then you can reconfigure the relation without denormalizing either entity.

### replace playlist tracks
Source: docs/source/exhibits/core/advanced/join/replace-playlist-tracks.ts

```ts
import { editRelations } from "atom.io"

import { playlistTracks } from "./declare-playlist-tracks.ts"

editRelations(playlistTracks, (relations) => {
	relations.replaceRelations(`playlist::road-trip`, [
		`track::dreams`,
		`track::landslide`,
		`track::rhiannon`,
	])
})
```

The join keeps both directions consistent. If the road trip playlist changes, `tracksInRoadTripState` updates. If a track appears in or disappears from a playlist, `playlistsUsingDreamsState` updates too.

Use `join` when you need that bidirectional consistency to be reactive, typed, and safe to reconfigure from the frontend.

## advanced

### atom effects

Atoms can declare `effects`, which are setup hooks that run when the atom is created.

Use `setSelf` when an external source should initialize or push values into the atom:

### hydrate an atom with set self
Source: docs/source/exhibits/core/advanced/effects/hydrate-an-atom-with-set-self.ts

```ts
import { atom } from "atom.io"

export const sidebarOpenAtom = atom<boolean>({
	key: `sidebarOpen`,
	default: false,
	effects: [
		({ setSelf }) => {
			const stored = localStorage.getItem(`sidebarOpen`)
			if (stored !== null) {
				setSelf(JSON.parse(stored))
			}
		},
	],
})
```

Use `onSet` when you want to react to changes after the atom updates:

### react to changes with on set
Source: docs/source/exhibits/core/advanced/effects/react-to-changes-with-on-set.ts

```ts
import { atom } from "atom.io"

export const searchQueryAtom = atom<string>({
	key: `searchQuery`,
	default: ``,
	effects: [
		({ onSet }) => {
			onSet(({ newValue }) => {
				console.log(`search query updated:`, newValue)
			})
		},
	],
})
```

These patterns are useful on their own, and `atom.io/web` builds on them with ready-made
browser effects such as storage and URL synchronization. See
[`atom.io/web`](/docs/web) for more examples of preincluded effects.

### async state

Often, the data you need is not immediately available. For example, you may need to fetch it from a server. `atom.io` offers natural support for `Promise` and `async/await` patterns.

### await your state
Source: docs/source/exhibits/core/advanced/async/await-your-state.ts

```ts
import http from "node:http"

import type { Loadable } from "atom.io"
import { atom, getState, resetState } from "atom.io"

const server = http.createServer((req, res) => {
	let data: Uint8Array[] = []
	req
		.on(`data`, (chunk) => data.push(chunk))
		.on(`end`, () => {
			res.writeHead(200, { "Content-Type": `text/plain` })
			res.end(`The best way to predict the future is to invent it.`)
			data = []
		})
})
server.listen(3000)

export const quoteAtom = atom<Loadable<string>, Error>({
	key: `quote`,
	default: async () => {
		const response = await fetch(`http://localhost:3000`)
		return response.text()
	},
	catch: [Error],
})

void getState(quoteAtom) // Promise { <pending> }
await getState(quoteAtom) // "The best way to predict the future is to invent it."
void getState(quoteAtom) // "The best way to predict the future is to invent it."
resetState(quoteAtom)
void getState(quoteAtom) // Promise { <pending> }
```

`Loadable<T>` is shorthand for "this state may currently be `T` or a `Promise<T>`".
While a load is pending, `getState` returns the promise. Because `await` is harmless for
plain values, you can safely `await getState(...)` whether the value is pending or
already loaded. After the promise resolves, later reads return the resolved value until
the state is reset or replaced.

### loadable selector
Source: docs/source/exhibits/core/advanced/async/loadable-selector.ts

```ts
import type { Loadable } from "atom.io"
import { atom, selector } from "atom.io"

function discoverCoinId() {
	const urlParams = new URLSearchParams(window.location.search)
	return urlParams.get(`coinId`) ?? `bitcoin`
}
export const coinIdAtom = atom<string>({
	key: `coinId`,
	default: discoverCoinId,
	effects: [
		({ setSelf }) => {
			const syncFromBrowser = () => {
				setSelf(discoverCoinId())
			}
			window.addEventListener(`popstate`, syncFromBrowser)
			return () => {
				window.removeEventListener(`popstate`, syncFromBrowser)
			}
		},
	],
})

export const coinPriceSelector = selector<Loadable<number>>({
	key: `coinPrice`,
	get: async ({ get }) => {
		const coinId = get(coinIdAtom)
		const response = await fetch(
			`https://api.coingecko.com/api/v3/coins/${coinId}`,
		)
		const json = await response.json()
		return json.market_data.current_price.usd
	},
})
```

Here is an example where we read a query parameter from the URL, then use it to fetch data from a server. This is a great pattern, because the selector's value will be cached as long as the URL parameter does not change.

For query data, fetched data, and RPC (remote procedure call) state that needs
clearer cache identity or more deliberate normalization, see the
[remote data guide](/docs/remote-data). It covers compact query atoms, loading
and suspense-like async patterns, loadable families keyed by input, and
local-index patterns for filtered or optimistic list screens.

### avoid race between promises
Source: docs/source/exhibits/core/advanced/async/avoid-race-between-promises.ts

```ts
import type { Loadable } from "atom.io"
import { atom, getState, setState } from "atom.io"

export const nameAtom = atom<Loadable<string>>({
	key: `name`,
	default: ``,
})
// resolve in 2 seconds
setState(
	nameAtom,
	new Promise<string>((resolve) =>
		setTimeout(() => {
			resolve(`one`)
		}, 2000),
	),
)
// resolve in 1 second
setState(
	nameAtom,
	new Promise<string>((resolve) =>
		setTimeout(() => {
			resolve(`two`)
		}, 1000),
	),
)
// "two" resolves first
// promise for "one" is set to be ignored
// "one" resolves, but is ignored
await new Promise((resolve) => setTimeout(resolve, 3000))
void getState(nameAtom) // "two"
```

If we update an async state more quickly than its promises resolve, only the last resolved value will be set into the state. All previous results will be discarded.

### catching errors

Regular atoms and pure selectors can declare a `catch` option.

When a matching error class is thrown, atom.io stores that error in the state instead of
rethrowing it. This gives the state a typed error channel, so code that reads it can
handle the expected failure case explicitly.

To make sure your declared error type stays aligned with the constructors in `catch`, use
[`atom.io/exact-catch-types`](/docs/eslint-plugin#exact-catch-types).

### catch an atom
Source: docs/source/exhibits/core/advanced/catching/catch-an-atom.ts

```ts
import { atom, getState } from "atom.io"

class MissingSessionError extends Error {
	public constructor() {
		super(`No active session`)
		this.name = `MissingSessionError`
	}
}

export const currentSessionIdAtom = atom<string, MissingSessionError>({
	key: `currentSessionId`,
	default: () => {
		throw new MissingSessionError()
	},
	catch: [MissingSessionError],
})

const result = getState(currentSessionIdAtom)

if (result instanceof MissingSessionError) {
	console.log(result.message) // -> "No active session"
}
```

The same pattern works for selectors:

### catch a selector
Source: docs/source/exhibits/core/advanced/catching/catch-a-selector.ts

```ts
import { atom, getState, selector } from "atom.io"

class UnauthorizedError extends Error {
	public constructor() {
		super(`You must sign in first`)
		this.name = `UnauthorizedError`
	}
}

const authTokenAtom = atom<string | null>({
	key: `authToken`,
	default: null,
})

export const viewerSelector = selector<{ id: string }, UnauthorizedError>({
	key: `viewer`,
	get: ({ get }) => {
		const authToken = get(authTokenAtom)
		if (authToken === null) {
			throw new UnauthorizedError()
		}
		return { id: authToken }
	},
	catch: [UnauthorizedError],
})

const result = getState(viewerSelector)

if (result instanceof UnauthorizedError) {
	console.log(result.message) // -> "You must sign in first"
}
```

This is especially helpful for `Loadable` state. If you read a caught loadable value with
[`useLoadable`](/docs/react#error-handling), the hook can expose the caught error
separately from the fallback value.

### mutable atoms

Most atom.io state should be modeled with regular immutable atoms. Sometimes, though, a
large collection changes frequently enough that copying the whole structure on every
update is not a good tradeoff.

For those cases, use `mutableAtom`.

### declare a mutable atom
Source: docs/source/exhibits/core/mutable/declare-a-mutable-atom.ts

```ts
import { getState, mutableAtom, setState } from "atom.io"
import { UList } from "atom.io/transceivers/u-list"

export const selectedTagKeysAtom = mutableAtom<UList<string>>({
	key: `selectedTagKeys`,
	class: UList,
})

getState(selectedTagKeysAtom).has(`typescript`) // -> false

setState(selectedTagKeysAtom, (selectedTagKeys) =>
	selectedTagKeys.add(`typescript`),
)

getState(selectedTagKeysAtom).has(`typescript`) // -> true
```

A mutable atom holds a `Transceiver`: a mutable object that knows how to report its own
changes to atom.io.

Read more in [the transceivers guide](/transceivers).

The built-in transceivers you will typically use are:

- `UList` from `atom.io/transceivers/u-list`: a trackable unordered set
- `OList` from `atom.io/transceivers/o-list`: a trackable ordered array

Read mutable atoms the same way you read regular atoms: with `getState`, `subscribe`,
or a UI adapter like `useO`.

Use `getJsonToken` when you want the mutable atom's JSON form as writable atom.io
state. It accepts either a mutable atom token, or a mutable atom family plus a key.

### get a json token
Source: docs/source/exhibits/core/mutable/get-a-json-token.ts

```ts
import { getJsonToken, getState, mutableAtom, setState } from "atom.io"
import { UList } from "atom.io/transceivers/u-list"

const selectedTagKeysAtom = mutableAtom<UList<string>>({
	key: `selectedTagKeys`,
	class: UList,
})

const selectedTagKeysJSON = getJsonToken(selectedTagKeysAtom)

getState(selectedTagKeysJSON) // -> []

setState(selectedTagKeysAtom, (selectedTagKeys) =>
	selectedTagKeys.add(`typescript`),
)

getState(selectedTagKeysJSON) // -> [`typescript`]
```

When writing one, prefer the setter callback form. Mutate the value inside the callback
and return it. That lets atom.io capture the transceiver's fine-grained update instead
of requiring you to replace the entire collection.
