# getting started

Source: docs/source/pages/docs/getting-started.mdx
URL: /docs/getting-started

# getting started

We think React is the habitat where the need for `atom.io` tends to form: local state
spreads across components, effects turn into synchronization logic, and wrapper hooks
start carrying more architecture than they meant to. So these guides are oriented around
common situations in an existing React app.

`atom.io` is not React-only. You can use it with Preact, Solid, and non-browser runtimes
too. But if you are evaluating it from a React codebase, start with
[one small state](#replace-react-use-state) that already exists in your app and port it
without changing the component's behavior.

<h2 id="install">install</h2>

Add `atom.io` to the app you already have:

### install

```bash
pnpm add atom.io
```

That's it! `atom.io` delivers its entire SDK in one package. (And it ships all the docs on this website in an easy-to-read format.)

<h2 id="replace-react-use-state">replace React.useState</h2>

Most React apps have a component like this: a local input value, a derived display value,
and an event handler that updates state.

### before product search use state
Source: docs/source/exhibits/guides/getting-started/before-product-search-use-state.tsx

```tsx
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

export function ProductSearch(): JSX.Element {
	const [query, setQuery] = React.useState(``)
	const normalizedQuery = query.trim().toLowerCase()
	const searchLabel = normalizedQuery || `everything`

	return (
		<section>
			<label htmlFor="product-search">Search products</label>
			<input
				id="product-search"
				value={query}
				onChange={(event) => {
					setQuery(event.currentTarget.value)
				}}
			/>
			<p>Searching for: {searchLabel}</p>
		</section>
	)
}
```

<h3 id="extract-state-to-an-atom">model state with atom</h3>

First, add an atom just outside the component. Keeping it in the same file is a good first
step: the token is module-local, so this does not create a new public state API for the
rest of the app.

### wip product search atom
Source: docs/source/exhibits/guides/getting-started/wip-product-search-atom.tsx

```tsx
import { atom } from "atom.io"
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

const productSearchQueryAtom = atom<string>({
	key: `productSearchQuery`,
	default: ``,
})

export function ProductSearch(): JSX.Element {
	const [query, setQuery] = React.useState(``)
	const normalizedQuery = query.trim().toLowerCase()
	const searchLabel = normalizedQuery || `everything`

	return (
		<section>
			<label htmlFor="product-search">Search products</label>
			<input
				id="product-search"
				value={query}
				onChange={(event) => {
					setQuery(event.currentTarget.value)
				}}
			/>
			<p>Searching for: {searchLabel}</p>
		</section>
	)
}
```

<h3 id="replace-use-state-with-use-i-and-use-o">
  {`replace useState with useI and useO`}
</h3>

Then replace `useState` with `useO` and `useI`. `useO` reads the atom's current value.
`useI` returns the setter for that atom.

### wip product search atom hooks
Source: docs/source/exhibits/guides/getting-started/wip-product-search-atom-hooks.tsx

```tsx
import { atom } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const productSearchQueryAtom = atom<string>({
	key: `productSearchQuery`,
	default: ``,
})

export function ProductSearch(): JSX.Element {
	const query = useO(productSearchQueryAtom)
	const setQuery = useI(productSearchQueryAtom)
	const normalizedQuery = query.trim().toLowerCase()
	const searchLabel = normalizedQuery || `everything`

	return (
		<section>
			<label htmlFor="product-search">Search products</label>
			<input
				id="product-search"
				value={query}
				onChange={(event) => {
					setQuery(event.currentTarget.value)
				}}
			/>
			<p>Searching for: {searchLabel}</p>
		</section>
	)
}
```

The component should behave the same way it did before. The difference is that the state
now has a stable token.

<h3 id="view-data-with-selector">derive a view with selector</h3>

Next, move the derived search label into a selector. Like the atom, this selector can stay
private to the module until another component needs it.

### wip product search selector
Source: docs/source/exhibits/guides/getting-started/wip-product-search-selector.tsx

```tsx
import { atom, selector } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const productSearchQueryAtom = atom<string>({
	key: `productSearchQuery`,
	default: ``,
})

const productSearchLabelSelector = selector<string>({
	key: `productSearchLabel`,
	get: ({ get }) => {
		const query = get(productSearchQueryAtom)
		const normalizedQuery = query.trim().toLowerCase()
		return normalizedQuery || `everything`
	},
})

export function ProductSearch(): JSX.Element {
	const query = useO(productSearchQueryAtom)
	const setQuery = useI(productSearchQueryAtom)
	const normalizedQuery = query.trim().toLowerCase()
	const searchLabel = normalizedQuery || `everything`

	return (
		<section>
			<label htmlFor="product-search">Search products</label>
			<input
				id="product-search"
				value={query}
				onChange={(event) => {
					setQuery(event.currentTarget.value)
				}}
			/>
			<p>Searching for: {searchLabel}</p>
		</section>
	)
}
```

<h3 id="read-the-selector-with-use-o">read the selector with useO</h3>

Finally, read the selector with `useO` too. The component now observes one atom for the
editable value and one selector for the value derived from it.

### after product search selector hooks
Source: docs/source/exhibits/guides/getting-started/after-product-search-selector-hooks.tsx

```tsx
import { atom, selector } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const productSearchQueryAtom = atom<string>({
	key: `productSearchQuery`,
	default: ``,
})

const productSearchLabelSelector = selector<string>({
	key: `productSearchLabel`,
	get: ({ get }) => {
		const query = get(productSearchQueryAtom)
		const normalizedQuery = query.trim().toLowerCase()
		return normalizedQuery || `everything`
	},
})

export function ProductSearch(): JSX.Element {
	const query = useO(productSearchQueryAtom)
	const setQuery = useI(productSearchQueryAtom)
	const searchLabel = useO(productSearchLabelSelector)

	return (
		<section>
			<label htmlFor="product-search">Search products</label>
			<input
				id="product-search"
				value={query}
				onChange={(event) => {
					setQuery(event.currentTarget.value)
				}}
			/>
			<p>Searching for: {searchLabel}</p>
		</section>
	)
}
```

You can keep both tokens private to this file, or export either token later if another
component needs to observe or update the same state.

<h2 id="replace-react-context-state">replace React context state</h2>

React context is often the next step after `useState`: one component controls a value,
another component reads it, and a provider gets added so they can stay in sync.

### before ticket filter context provider
Source: docs/source/exhibits/guides/getting-started/before-ticket-filter-context-provider.tsx

```tsx
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

type TicketStatus = `closed` | `open`

type TicketFilterContextValue = {
	status: TicketStatus
	setStatus: React.Dispatch<React.SetStateAction<TicketStatus>>
}

const TicketFilterContext = React.createContext<TicketFilterContextValue | null>(
	null,
)

function useTicketFilter(): TicketFilterContextValue {
	const value = React.useContext(TicketFilterContext)
	if (!value) {
		throw new Error(`useTicketFilter must be used inside TicketFilterProvider`)
	}
	return value
}

function TicketFilterProvider(props: React.PropsWithChildren): JSX.Element {
	const [status, setStatus] = React.useState<TicketStatus>(`open`)

	return (
		<TicketFilterContext.Provider value={{ status, setStatus }}>
			{props.children}
		</TicketFilterContext.Provider>
	)
}

function TicketStatusSelect(): JSX.Element {
	const { status, setStatus } = useTicketFilter()

	return (
		<label>
			Status
			<select
				value={status}
				onChange={(event) => {
					setStatus(event.currentTarget.value as TicketStatus)
				}}
			>
				<option value="open">Open</option>
				<option value="closed">Closed</option>
			</select>
		</label>
	)
}

function TicketQueueHeading(): JSX.Element {
	const { status } = useTicketFilter()

	return <h2>{status === `open` ? `Open tickets` : `Closed tickets`}</h2>
}

export function TicketQueue(): JSX.Element {
	return (
		<TicketFilterProvider>
			<TicketStatusSelect />
			<TicketQueueHeading />
		</TicketFilterProvider>
	)
}
```

<h3 id="replace-the-context-wrapper-with-an-atom">
  {`replace the context wrapper with an atom`}
</h3>

Move the shared value into an atom, then read or update it directly from each component.
The two components still share the same state, but there is no provider wrapper or custom
context hook to maintain.

### after ticket filter atom
Source: docs/source/exhibits/guides/getting-started/after-ticket-filter-atom.tsx

```tsx
import { atom } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

type TicketStatus = `closed` | `open`

const ticketStatusAtom = atom<TicketStatus>({
	key: `ticketStatus`,
	default: `open`,
})

function TicketStatusSelect(): JSX.Element {
	const status = useO(ticketStatusAtom)
	const setStatus = useI(ticketStatusAtom)

	return (
		<label>
			Status
			<select
				value={status}
				onChange={(event) => {
					setStatus(event.currentTarget.value as TicketStatus)
				}}
			>
				<option value="open">Open</option>
				<option value="closed">Closed</option>
			</select>
		</label>
	)
}

function TicketQueueHeading(): JSX.Element {
	const status = useO(ticketStatusAtom)

	return <h2>{status === `open` ? `Open tickets` : `Closed tickets`}</h2>
}

export function TicketQueue(): JSX.Element {
	return (
		<>
			<TicketStatusSelect />
			<TicketQueueHeading />
		</>
	)
}
```

<h2 id="replace-zustand-store">replace Zustand store</h2>

If you have Zustand in the app, you probably introduced it for exactly this kind of
state: shared values, selectors, and actions living together in one hook-shaped store.

### before shopping cart zustand store
Source: docs/source/exhibits/guides/getting-started/before-shopping-cart-zustand-store.tsx

```tsx
import type { JSX } from "react/jsx-runtime"
import { create } from "zustand"

type CartProduct = {
	id: string
	name: string
	price: number
}

type CartItem = CartProduct & {
	quantity: number
}

type CartStore = {
	items: CartItem[]
	couponCode: string
	addItem: (product: CartProduct) => void
	removeItem: (id: string) => void
	setCouponCode: (couponCode: string) => void
}

const featuredProduct: CartProduct = {
	id: `notebook`,
	name: `Notebook`,
	price: 18,
}

const useCartStore = create<CartStore>((set) => ({
	items: [],
	couponCode: ``,
	addItem: (product) => {
		set((state) => {
			const existingItem = state.items.find((item) => item.id === product.id)
			if (existingItem) {
				return {
					items: state.items.map((item) =>
						item.id === product.id
							? { ...item, quantity: item.quantity + 1 }
							: item,
					),
				}
			}
			return { items: [...state.items, { ...product, quantity: 1 }] }
		})
	},
	removeItem: (id) => {
		set((state) => ({
			items: state.items.filter((item) => item.id !== id),
		}))
	},
	setCouponCode: (couponCode) => {
		set({ couponCode })
	},
}))

const selectCartItemCount = (state: CartStore): number =>
	state.items.reduce((total, item) => total + item.quantity, 0)

const selectCartSubtotal = (state: CartStore): number =>
	state.items.reduce((total, item) => total + item.price * item.quantity, 0)

function AddToCartButton(): JSX.Element {
	const itemCount = useCartStore(selectCartItemCount)
	const addItem = useCartStore((state) => state.addItem)

	return (
		<button
			type="button"
			onClick={() => {
				addItem(featuredProduct)
			}}
		>
			Add notebook ({itemCount})
		</button>
	)
}

function CartItems(): JSX.Element {
	const items = useCartStore((state) => state.items)
	const removeItem = useCartStore((state) => state.removeItem)

	if (items.length === 0) {
		return <p>Your cart is empty.</p>
	}

	return (
		<ul>
			{items.map((item) => (
				<li key={item.id}>
					{item.name} x{item.quantity}
					<button
						type="button"
						onClick={() => {
							removeItem(item.id)
						}}
					>
						Remove
					</button>
				</li>
			))}
		</ul>
	)
}

function CouponInput(): JSX.Element {
	const couponCode = useCartStore((state) => state.couponCode)
	const setCouponCode = useCartStore((state) => state.setCouponCode)

	return (
		<label>
			Coupon
			<input
				value={couponCode}
				onChange={(event) => {
					setCouponCode(event.currentTarget.value)
				}}
			/>
		</label>
	)
}

function CartSummary(): JSX.Element {
	const subtotal = useCartStore(selectCartSubtotal)
	const couponCode = useCartStore((state) => state.couponCode)
	const discount = couponCode === `SAVE10` ? subtotal * 0.1 : 0
	const total = subtotal - discount

	return <p>Total: ${total.toFixed(2)}</p>
}

export function ShoppingCart(): JSX.Element {
	return (
		<section>
			<AddToCartButton />
			<CartItems />
			<CouponInput />
			<CartSummary />
		</section>
	)
}
```

<h3 id="split-the-store-into-atoms-selectors-and-transactions">
  {`split the store into atoms, selectors, and transactions`}
</h3>

Move collection keys into one atom, each item into an atom family, derived views into
selectors, and store actions into transactions. Components still subscribe to exactly
the state they need, but each piece now has its own `atom.io` token.

### after shopping cart atoms selectors transactions
Source: docs/source/exhibits/guides/getting-started/after-shopping-cart-atoms-selectors-transactions.tsx

```tsx
import { atom, atomFamily, runTransaction, selector, transaction } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

type CartProduct = {
	id: string
	name: string
	price: number
}

type CartItem = CartProduct & {
	quantity: number
}

const FEATURED_PRODUCT: CartProduct = {
	id: `notebook`,
	name: `Notebook`,
	price: 18,
}

const cartItemsKeysAtom = atom<string[]>({
	key: `cartItemsKeys`,
	default: [],
})

const cartItemAtoms = atomFamily<CartItem, string>({
	key: `cartItem`,
	default: (id) => ({
		id,
		name: id,
		price: 0,
		quantity: 0,
	}),
})

const couponCodeAtom = atom<string>({
	key: `couponCode`,
	default: ``,
})

const cartItemCountSelector = selector<number>({
	key: `cartItemCount`,
	get: ({ get }) =>
		get(cartItemsKeysAtom).reduce(
			(total, id) => total + get(cartItemAtoms, id).quantity,
			0,
		),
})

const cartSubtotalSelector = selector<number>({
	key: `cartSubtotal`,
	get: ({ get }) =>
		get(cartItemsKeysAtom).reduce((total, id) => {
			const item = get(cartItemAtoms, id)
			return total + item.price * item.quantity
		}, 0),
})

const cartItemsSelector = selector<CartItem[]>({
	key: `cartItems`,
	get: ({ get }) => get(cartItemsKeysAtom).map((id) => get(cartItemAtoms, id)),
})

const cartTotalSelector = selector<number>({
	key: `cartTotal`,
	get: ({ get }) => {
		const subtotal = get(cartSubtotalSelector)
		const couponCode = get(couponCodeAtom)
		const discount = couponCode === `SAVE10` ? subtotal * 0.1 : 0
		return subtotal - discount
	},
})

const addCartItemTransaction = transaction<(product: CartProduct) => void>({
	key: `addCartItem`,
	do: ({ get, set }, product) => {
		const itemKeys = get(cartItemsKeysAtom)
		if (itemKeys.includes(product.id)) {
			set(cartItemAtoms, product.id, (item) => ({
				...item,
				quantity: item.quantity + 1,
			}))
			return
		}
		set(cartItemsKeysAtom, [...itemKeys, product.id])
		set(cartItemAtoms, product.id, { ...product, quantity: 1 })
	},
})

const addCartItem = runTransaction(addCartItemTransaction)

const removeCartItemTransaction = transaction<(id: string) => void>({
	key: `removeCartItem`,
	do: ({ dispose, get, set }, id) => {
		const itemKeys = get(cartItemsKeysAtom)
		if (!itemKeys.includes(id)) return

		set(
			cartItemsKeysAtom,
			itemKeys.filter((itemId) => itemId !== id),
		)
		dispose(cartItemAtoms, id)
	},
})

const removeCartItem = runTransaction(removeCartItemTransaction)

function AddToCartButton(): JSX.Element {
	const itemCount = useO(cartItemCountSelector)

	return (
		<button
			type="button"
			onClick={() => {
				addCartItem(FEATURED_PRODUCT)
			}}
		>
			Add notebook ({itemCount})
		</button>
	)
}

function CartItems(): JSX.Element {
	const items = useO(cartItemsSelector)

	if (items.length === 0) {
		return <p>Your cart is empty.</p>
	}

	return (
		<ul>
			{items.map((item) => (
				<li key={item.id}>
					{item.name} x{item.quantity}
					<button
						type="button"
						onClick={() => {
							removeCartItem(item.id)
						}}
					>
						Remove
					</button>
				</li>
			))}
		</ul>
	)
}

function CouponInput(): JSX.Element {
	const couponCode = useO(couponCodeAtom)
	const setCouponCode = useI(couponCodeAtom)

	return (
		<label>
			Coupon
			<input
				value={couponCode}
				onChange={(event) => {
					setCouponCode(event.currentTarget.value)
				}}
			/>
		</label>
	)
}

function CartSummary(): JSX.Element {
	const total = useO(cartTotalSelector)

	return <p>Total: ${total.toFixed(2)}</p>
}

export function ShoppingCart(): JSX.Element {
	return (
		<section>
			<AddToCartButton />
			<CartItems />
			<CouponInput />
			<CartSummary />
		</section>
	)
}
```

<h2 id="replace-react-use-callback">replace React useCallback</h2>

Some event handlers update more than one independent state. In a task board, marking a
task done might remove it from one list and add it to another. If validation can throw
between those writes, the callback has to restore both states by hand.

### before task board use callback rollback
Source: docs/source/exhibits/guides/getting-started/before-task-board-use-callback-rollback.tsx

```tsx
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

const BLOCKED_TASK_IDS = new Set([`billing`])

export function TaskBoard(): JSX.Element {
	const [todoIds, setTodoIds] = React.useState([`billing`, `docs`])
	const [doneIds, setDoneIds] = React.useState([`setup`])
	const [error, setError] = React.useState<string | null>(null)

	const markDone = React.useCallback(
		(taskId: string) => {
			const previousTodoIds = todoIds
			const previousDoneIds = doneIds

			try {
				setError(null)
				setTodoIds((current) => current.filter((id) => id !== taskId))

				if (BLOCKED_TASK_IDS.has(taskId)) {
					throw new Error(`Blocked tasks need approval before they can move.`)
				}

				setDoneIds((current) => [taskId, ...current])
			} catch (thrown) {
				setTodoIds(previousTodoIds)
				setDoneIds(previousDoneIds)
				setError(
					thrown instanceof Error ? thrown.message : `Could not move task.`,
				)
			}
		},
		[todoIds, doneIds],
	)

	return (
		<section>
			{error ? <p role="alert">{error}</p> : null}

			<h2>To do</h2>
			<ul>
				{todoIds.map((taskId) => (
					<li key={taskId}>
						{taskId}
						<button
							type="button"
							onClick={() => {
								markDone(taskId)
							}}
						>
							Mark done
						</button>
					</li>
				))}
			</ul>

			<h2>Done</h2>
			<ul>
				{doneIds.map((taskId) => (
					<li key={taskId}>{taskId}</li>
				))}
			</ul>
		</section>
	)
}
```

<h3 id="replace-the-callback-with-a-transaction">
  {`replace the callback with a transaction`}
</h3>

Move the coordinated writes into a transaction, and keep the error message in atom state
too. If the transaction throws, `atom.io` aborts it: the intermediate changes stay in the
transaction store and never reach the store React observes.

### wip task board transaction
Source: docs/source/exhibits/guides/getting-started/wip-task-board-transaction.tsx

```tsx
import { atom, runTransaction, transaction } from "atom.io"
import { useI, useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const BLOCKED_TASK_IDS = new Set([`billing`])

const todoIdsAtom = atom<string[]>({
	key: `todoIds`,
	default: [`billing`, `docs`],
})

const doneIdsAtom = atom<string[]>({
	key: `doneIds`,
	default: [`setup`],
})

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

const markDoneTransaction = transaction<(taskId: string) => void>({
	key: `markDone`,
	do: ({ get, set }, taskId) => {
		set(
			todoIdsAtom,
			get(todoIdsAtom).filter((id) => id !== taskId),
		)

		if (BLOCKED_TASK_IDS.has(taskId)) {
			throw new Error(`Blocked tasks need approval before they can move.`)
		}

		set(doneIdsAtom, [taskId, ...get(doneIdsAtom)])
	},
})

const markDone = runTransaction(markDoneTransaction)

export function TaskBoard(): JSX.Element {
	const todoIds = useO(todoIdsAtom)
	const doneIds = useO(doneIdsAtom)
	const error = useO(taskBoardErrorAtom)
	const setError = useI(taskBoardErrorAtom)

	return (
		<section>
			{error ? <p role="alert">{error}</p> : null}

			<h2>To do</h2>
			<ul>
				{todoIds.map((taskId) => (
					<li key={taskId}>
						{taskId}
						<button
							type="button"
							onClick={() => {
								try {
									setError(null)
									markDone(taskId)
								} catch (thrown) {
									setError(
										thrown instanceof Error
											? thrown.message
											: `Could not move task.`,
									)
								}
							}}
						>
							Mark done
						</button>
					</li>
				))}
			</ul>

			<h2>Done</h2>
			<ul>
				{doneIds.map((taskId) => (
					<li key={taskId}>{taskId}</li>
				))}
			</ul>
		</section>
	)
}
```

<h3 id="move-the-handler-into-a-nested-transaction">
  {`move the handler into a nested transaction`}
</h3>

Finally, make the handler itself a transaction. It clears the error, `run`s the
error-prone transaction, catches any failure, and writes the error message. The component
only observes state and starts the transaction.

### after task board nested transaction
Source: docs/source/exhibits/guides/getting-started/after-task-board-nested-transaction.tsx

```tsx
import { atom, runTransaction, transaction } from "atom.io"
import { useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const BLOCKED_TASK_IDS = new Set([`billing`])

const todoIdsAtom = atom<string[]>({
	key: `todoIds`,
	default: [`billing`, `docs`],
})

const doneIdsAtom = atom<string[]>({
	key: `doneIds`,
	default: [`setup`],
})

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

const markDoneTransaction = transaction<(taskId: string) => void>({
	key: `markDone`,
	do: ({ get, set }, taskId) => {
		set(
			todoIdsAtom,
			get(todoIdsAtom).filter((id) => id !== taskId),
		)

		if (BLOCKED_TASK_IDS.has(taskId)) {
			throw new Error(`Blocked tasks need approval before they can move.`)
		}

		set(doneIdsAtom, [taskId, ...get(doneIdsAtom)])
	},
})

const tryMarkDoneTransaction = transaction<(taskId: string) => void>({
	key: `tryMarkDone`,
	do: ({ run, set }, taskId) => {
		try {
			set(taskBoardErrorAtom, null)
			run(markDoneTransaction)(taskId)
		} catch (thrown) {
			set(
				taskBoardErrorAtom,
				thrown instanceof Error ? thrown.message : `Could not move task.`,
			)
		}
	},
})

const markDone = runTransaction(tryMarkDoneTransaction)

export function TaskBoard(): JSX.Element {
	const todoIds = useO(todoIdsAtom)
	const doneIds = useO(doneIdsAtom)
	const error = useO(taskBoardErrorAtom)

	return (
		<section>
			{error ? <p role="alert">{error}</p> : null}

			<h2>To do</h2>
			<ul>
				{todoIds.map((taskId) => (
					<li key={taskId}>
						{taskId}
						<button
							type="button"
							onClick={() => {
								markDone(taskId)
							}}
						>
							Mark done
						</button>
					</li>
				))}
			</ul>

			<h2>Done</h2>
			<ul>
				{doneIds.map((taskId) => (
					<li key={taskId}>{taskId}</li>
				))}
			</ul>
		</section>
	)
}
```

<h2 id="replace-react-query">replace React Query</h2>

React Query is a common first home for server state: a query function, a query key, and
component branches for loading, error, and refresh states.

### before dashboard summary react query
Source: docs/source/exhibits/guides/getting-started/before-dashboard-summary-react-query.tsx

```tsx
import { useQuery } from "@tanstack/react-query"
import type { JSX } from "react/jsx-runtime"

type DashboardStats = {
	openTicketCount: number
	overdueTicketCount: number
	medianResponseTime: string
}

async function fetchDashboardStats(): Promise<DashboardStats> {
	const response = await fetch(`/api/dashboard/stats`)
	if (!response.ok) {
		throw new Error(`Could not load dashboard stats.`)
	}
	return response.json()
}

export function DashboardSummary(): JSX.Element {
	const statsQuery = useQuery({
		queryKey: [`dashboardStats`],
		queryFn: fetchDashboardStats,
	})

	if (statsQuery.isPending) {
		return <p>Loading dashboard...</p>
	}

	if (statsQuery.isError) {
		return <p role="alert">{statsQuery.error.message}</p>
	}

	return (
		<section>
			<h2>Support dashboard</h2>
			<p>{statsQuery.data.openTicketCount} open tickets</p>
			<p>{statsQuery.data.overdueTicketCount} overdue</p>
			<p>Median response: {statsQuery.data.medianResponseTime}</p>
			{statsQuery.isFetching ? <small>Refreshing...</small> : null}
		</section>
	)
}
```

<h3 id="replace-use-query-with-a-loadable-atom">
  {`replace useQuery with a Loadable atom`}
</h3>

Move the query function into a `Loadable` atom, then read it with `useLoadable`. The
component still gets a value, a loading flag, and an error, but the loading state now has
an `atom.io` token that other components can share.

### after dashboard summary loadable atom
Source: docs/source/exhibits/guides/getting-started/after-dashboard-summary-loadable-atom.tsx

```tsx
import { atom, type Loadable } from "atom.io"
import { useLoadable } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

type DashboardStats = {
	openTicketCount: number
	overdueTicketCount: number
	medianResponseTime: string
}

const EMPTY_DASHBOARD_STATS: DashboardStats = {
	openTicketCount: 0,
	overdueTicketCount: 0,
	medianResponseTime: `--`,
}

async function fetchDashboardStats(): Promise<DashboardStats> {
	const response = await fetch(`/api/dashboard/stats`)
	if (!response.ok) {
		throw new Error(`Could not load dashboard stats.`)
	}
	return response.json()
}

const dashboardStatsAtom = atom<Loadable<DashboardStats>, Error>({
	key: `dashboardStats`,
	default: fetchDashboardStats,
	catch: [Error],
})

export function DashboardSummary(): JSX.Element {
	const stats = useLoadable(dashboardStatsAtom, EMPTY_DASHBOARD_STATS)

	if (stats.error) {
		return <p role="alert">{stats.error.message}</p>
	}

	return (
		<section>
			<h2>Support dashboard</h2>
			<p>{stats.value.openTicketCount} open tickets</p>
			<p>{stats.value.overdueTicketCount} overdue</p>
			<p>Median response: {stats.value.medianResponseTime}</p>
			{stats.loading ? <small>Refreshing...</small> : null}
		</section>
	)
}
```

<h3 id="do-not-refresh-loadables-on-mount">
  {`do not refresh Loadables on mount`}
</h3>

The `Loadable` atom is already the loading trigger. Reading it with `useLoadable`
starts the request when the value is missing.

Avoid adding a mount effect that resets the same atom:

### reset on mount
Source: docs/source/exhibits/guides/getting-started/reset-dashboard-stats-on-mount.tsx#reset-dashboard-stats-on-mount

```tsx
React.useEffect(() => {
	resetState(dashboardStatsAtom) // ❌ do not reset on mount; duplicates async work
}, [])
```

That reset runs after the first read has already started loading. In React development
`StrictMode`, the mount effect can run more than once, causing duplicate requests.

Prefer the pattern in the example above: read the `Loadable` directly and reset it only
from explicit refresh, retry, mutation, or test setup flows.

For keyed records, make the request identity part of the atom family key:

### keyed loadable family
Source: docs/source/exhibits/guides/getting-started/keyed-dashboard-stats-loadable-family.tsx#keyed-dashboard-stats-loadable-family

```tsx
const dashboardStatsAtoms = atomFamily<
	Loadable<DashboardStats>,
	DashboardId,
	Error
>({
	key: `dashboardStats`,
	default: fetchDashboardStats,
	catch: [Error],
})

function DashboardSummary({ dashboardId }: { dashboardId: DashboardId }) {
	const stats = useLoadable(
		dashboardStatsAtoms,
		dashboardId,
		EMPTY_DASHBOARD_STATS,
	)

	return <p>{stats.value.openTicketCount} open tickets</p>
}
```

For richer query data and fetched data screens, especially with type-safe RPC
(remote procedure call) clients such as [tRPC](https://trpc.io),
[oRPC](https://orpc.dev), or
[Elysia](https://elysiajs.com) + Eden, see the
[remote data guide](/docs/remote-data) for loading and suspense-like patterns.

<h2 id="replace-react-localStorage-effect">
  {`replace React localStorage effect`}
</h2>

Another common React pattern is synchronizing local UI state with a browser API. For
example, a sidebar preference might read from `localStorage` when the component mounts,
then write back whenever the user changes it.

### before sidebar local storage use effect
Source: docs/source/exhibits/guides/getting-started/before-sidebar-local-storage-use-effect.tsx

```tsx
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

const SIDEBAR_STORAGE_KEY = `sidebarCollapsed`

export function SidebarLocalStoragePreference(): JSX.Element {
	const [collapsed, setCollapsed] = React.useState(
		() => localStorage.getItem(SIDEBAR_STORAGE_KEY) === `true`,
	)

	React.useEffect(() => {
		localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed))
	}, [collapsed])

	return (
		<label>
			<input
				type="checkbox"
				checked={collapsed}
				onChange={(event) => {
					setCollapsed(event.currentTarget.checked)
				}}
			/>
			Collapse sidebar
		</label>
	)
}
```

<h3 id="replace-use-effect-with-storage-sync">
  {`replace useEffect with storageSync`}
</h3>

Now the component only reads and updates atom state. The local-storage behavior lives with
the state definition instead of in the render tree.

### after sidebar local storage sync atom
Source: docs/source/exhibits/guides/getting-started/after-sidebar-local-storage-sync-atom.tsx

```tsx
import { atom } from "atom.io"
import { useI, useO } from "atom.io/react"
import { storageSync } from "atom.io/web"
import type { JSX } from "react/jsx-runtime"

const SIDEBAR_STORAGE_KEY = `sidebarCollapsed`

const sidebarCollapsedAtom = atom<boolean>({
	key: `sidebarCollapsed`,
	default: false,
	effects: [storageSync(localStorage, JSON, SIDEBAR_STORAGE_KEY)],
})

export function SidebarLocalStoragePreference(): JSX.Element {
	const collapsed = useO(sidebarCollapsedAtom)
	const setCollapsed = useI(sidebarCollapsedAtom)

	return (
		<label>
			<input
				type="checkbox"
				checked={collapsed}
				onChange={(event) => {
					setCollapsed(event.currentTarget.checked)
				}}
			/>
			Collapse sidebar
		</label>
	)
}
```

<h2 id="replace-react-setInterval-effect">
  {`replace React setInterval effect`}
</h2>

Timers are another common effect hiding inside React components. A last-saved indicator,
for example, might keep a ticking `now` value just so the label can update every second.

### before last saved interval use effect
Source: docs/source/exhibits/guides/getting-started/before-last-saved-interval-use-effect.tsx

```tsx
import * as React from "react"
import type { JSX } from "react/jsx-runtime"

const LAST_SAVED_AT = Date.now() - 12_000

export function LastSavedIndicator(): JSX.Element {
	const [now, setNow] = React.useState(Date.now())
	const seconds = Math.floor((now - LAST_SAVED_AT) / 1000)
	const label = seconds <= 0 ? `Saved just now` : `Saved ${seconds}s ago`

	React.useEffect(() => {
		const interval = window.setInterval(() => {
			setNow(Date.now())
		}, 1000)

		return () => {
			window.clearInterval(interval)
		}
	}, [])

	return <output>{label}</output>
}
```

<h3 id="replace-use-effect-with-timer-atom">
  {`replace useEffect with a timer atom`}
</h3>

Move the interval into a custom atom effect, then derive the display label with a selector.
The component only reads the selector, while the timer and cleanup behavior live with the
state definition.

### after last saved timer atom selector
Source: docs/source/exhibits/guides/getting-started/after-last-saved-timer-atom-selector.tsx

```tsx
import { atom, selector } from "atom.io"
import { useO } from "atom.io/react"
import type { JSX } from "react/jsx-runtime"

const LAST_SAVED_AT = Date.now() - 12_000

const lastSavedTimerAtom = atom<number>({
	key: `lastSavedTimer`,
	default: Date.now(),
	effects: [
		({ setSelf }) => {
			const interval = window.setInterval(() => {
				setSelf(Date.now())
			}, 1000)

			return () => {
				window.clearInterval(interval)
			}
		},
	],
})

const lastSavedLabelSelector = selector<string>({
	key: `lastSavedLabel`,
	get: ({ get }) => {
		const now = get(lastSavedTimerAtom)
		const seconds = Math.floor((now - LAST_SAVED_AT) / 1000)
		return seconds <= 0 ? `Saved just now` : `Saved ${seconds}s ago`
	},
})

export function LastSavedIndicator(): JSX.Element {
	const label = useO(lastSavedLabelSelector)

	return <output>{label}</output>
}
```
