Skip to content

Extensions

withAbort

Extension to add abort handling to actions and computed atoms.

Example 1

// last-in-win: only the last request matters
const fetchUser = action(async (id: number) => {
const response = await wrap(fetch(`/api/user/${id}`))
return response.json()
}).extend(withAbort())
fetchUser(1) // will be aborted
fetchUser(2) // will be aborted
fetchUser(3) // this one wins

Example 2

// first-in-win: ignore subsequent calls until the first completes
const fetchOnce = action(async () => {
await wrap(fetch('/api/data'))
}).extend(withAbort('first-in-win'))
fetchOnce() // runs
fetchOnce() // ignored, returns previous promise
fetchOnce() // ignored, returns previous promise

Example 3

// manual with manual abort (useful for polling/long-running tasks)
const poll = action(async () => {
while (true) {
await wrap(sleep(1000))
doSome()
}
}).extend(withAbort('manual'))
// start
poll()
// stop
poll.abort()

withChangeHook

Executes a callback whenever the target atom’s state changes.

This extension is essential for creating stable, declarative connections between independent modules or features. The hook fires in the “Hooks” phase of Reatom’s lifecycle (after Updates, before Computations), making it perfect for triggering side effects or synchronizing state across module boundaries.

When to use:*

  • Creating stable connections between features that shouldn’t depend on each

other directly

  • Triggering validation when a field’s value or state changes

  • Syncing derived state in response to source state changes

  • Managing side effects like DOM updates or analytics based on state changes

  • Coordinating behavior across module boundaries without coupling them

When NOT to use:*

  • In dynamic features, like from computed factories (use take or effect and

ifChanged instead)

  • When a regular computed dependency would suffice

  • For connection/disconnection events (use withConnectHook instead)

The callback receives the new state and previous state. It only fires when the state actually changes (referential inequality check via Object.is). The callback executes within the same reactive context, so you can safely call other atoms and actions.

Example 1

// Basic usage: React to atom state changes
const theme = reatomEnum(['light', 'dark', 'system']).extend(
withChangeHook((state, prevState) => {
document.body.classList.remove(prevState)
document.body.classList.add(state)
}),
)

Example 2

// Stable feature connection: Analytics tracking
// In userModule.ts
export const userAtom = atom({ id: null, name: '' }, 'user')
// In analyticsModule.ts
import { userAtom } from './userModule'
userAtom.extend(
withChangeHook((user, prevUser) => {
if (user.id !== prevUser?.id) {
analytics.identify(user.id, { name: user.name })
}
}),
)

addChangeHook

Dynamically adds a change hook to an existing atom and returns a function to remove it.

Unlike withChangeHook which is applied at atom definition time, addChangeHook allows you to add and remove hooks at runtime. This is useful for temporary subscriptions or when you need conditional hook behavior that can be enabled/disabled dynamically.

This feature is rarely needed, you should prefer using effect with ifChanged or take instead.

withCallHook

Executes a callback whenever the target action is called.

This extension enables you to react to action invocations, making it invaluable for creating stable connections between independent features. The hook fires in the “Hooks” phase (after Updates, before Computations) and receives both the action’s return value and its parameters.

When to use:*

  • Creating stable cross-module connections that react to specific actions

  • Tracking action calls for analytics, logging, or debugging

  • Triggering side effects in response to action completions

  • Coordinating behavior between independent features without coupling them

  • Implementing event-driven communication patterns

When NOT to use:*

  • In dynamic features, like from computed factories (take or effect and

getCalls instead)

  • When you can achieve the same goal with direct action composition

For actions extended with withAsync, you can also hook into .onFulfill , .onReject , or .onSettle to react to async completion events.

Example 1

// Cross-module coordination: Analytics tracking
// In checkoutModule.ts
export const submitOrder = action(async (order) => {
const result = await api.submitOrder(order)
return result
}, 'submitOrder')
// In analyticsModule.ts
import { submitOrder } from './checkoutModule'
submitOrder.extend(
withCallHook((promise, params) => {
const [order] = params
analytics.track('new_order', {
orderId: order.id,
total: order.total,
})
}),
)

Example 2

// Stable feature connection: Form submission tracking
const fetch = action(async (param: number) => {
const data = await api.fetch(param)
return data
}, 'fetch').extend(withAsync())
fetch.onFulfill.extend(
withCallHook((call) => {
console.log('Fetch completed', call.payload)
}),
)

addCallHook

Dynamically adds a call hook to an existing action and returns a function to remove it.

Unlike withCallHook which is applied at action definition time, addCallHook allows you to add and remove hooks at runtime. This is useful for temporary subscriptions, conditional hook behavior, or when integrating with external systems that need to be connected and disconnected dynamically.

This feature is rarely needed, you should prefer using effect with getCalls or take instead.

withComputed

A middleware extension that enhances an atom with computed capabilities.

isInit

Checks if the current execution context is within the initialization of the current atom.

Example

const search = atom('', 'search').extend(withSearchParams('search'))
const page = atom(1, 'page').extend(
withSearchParams('page'),
withComputed((state) => {
search() // subscribe to the search changes
// do NOT drop the persisted state on init
return isInit() ? state : 1
}),
)

withInit

Define dynamically computed initial value for an atom.

Typically, you can use just an init callback in atom first argument: atom(() => new Date()). But if you need to add initial callback after the atom creation, so there this extensions is useful.

Example 1

const something = reatomSomething().extend(
withInit((initState) => ({ ...initState, ...additions })),
)

Example 2

const myData = atom(null, 'myData')
if (meta.env.TEST) {
myData.extend(withInit(mockData))
}

withInitHook

Extension that runs the passed hook when the atom is initialized.

Example

const userAtom = atom({ id: 1, name: 'John' }).extend(
withInitHook((initState) => {
// Perform any setup logic here
analytics.track('user_loaded', initState)
}),
)

SUSPENSE

Internal suspense cache mapping promises to their settlement state. Do not use it directly, only for libraries!

settled

Checks if a promise is settled and returns its value or fallback. If the promise is fulfilled, returns the resolved value. If the promise is rejected, throws the error. If the promise is pending, returns the fallback value (defaults to undefined).

Uses an internal WeakMap cache to track promise states across calls.

Example

const promise = Promise.resolve(42)
await promise
const value = settled(promise) // 42

withSuspense

Extension that adds suspense support to async atoms. Creates a suspended computed atom that tracks the resolved value of promises and throws the promise when pending (for React Suspense compatibility).

The suspended atom will:

  • Return the resolved value immediately if the promise is already fulfilled

  • Throw the promise if it’s still pending (allowing Suspense boundaries to

catch it)

  • Propagate errors if the promise is rejected

  • Automatically update when the promise resolves

Example

const data = computed(async () => {
const response = await fetch('/api/data')
return response.json()
}, 'data').extend(withSuspense())
// Subscribe to resolved values
subscribe(data.suspended, (value) => {
console.log('Resolved:', value)
})
// Use in React component with Suspense
function Component() {
const value = useAtom(data.suspended) // throws promise if pending
return <div>{value}</div>
}

suspense

Helper function to access the suspended value of an atom. Automatically applies withSuspense() extension if the atom doesn’t already have it.

This function:

  • Returns the resolved value if the promise is fulfilled

  • Throws the promise if it’s still pending (for Suspense boundaries)

  • Throws the error if the promise is rejected

If withSuspense is already applied with different preserve options, the behavior may be inconsistent. Consider applying withSuspense() explicitly to control options.

Example

const data = computed(async () => {
const response = await fetch('/api/data')
return response.json()
}, 'data')
// Automatically applies withSuspense() and returns suspended value
const result = computed(() => {
try {
return suspense(data) // throws promise if pending
} catch (promise) {
if (promise instanceof Promise) {
// Handle pending state
return undefined
}
throw promise // Re-throw errors
}
}, 'result')

withSuspenseInit

Extension that enables asynchronous initialization for synchronous atoms. This feature bridges async data loading with sync atom semantics.

During initialization, if the result is a Promise, it throws the promise (suspense pattern) and schedules setting the atom’s state when resolved. After initialization completes, the atom operates fully synchronously.

This is perfect for local-first architectures: load data asynchronously on init, then work with it synchronously. Combine with withChangeHook to sync changes back to a server or database.

Without callback*: Transforms Atom<Promise<State>> into Atom<State>. The atom’s async initializer is unwrapped, and consumers receive the resolved value.

Example 1

const userSettings = atom(async () => {
const response = await fetch('/api/settings')
return response.json()
}).extend(withSuspenseInit())
// Type: Atom<Settings> (not Atom<Promise<Settings>>)
effect(() => {
// After init completes, reads are synchronous
const settings = userSettings()
console.log(settings.theme)
})

Example 2

// With callback: Provides an async initializer for any atom type, keeping the original state type.
// Local-first pattern: async init + sync operations + sync-back
const todos = atom<Todo[]>([]).extend(
withSuspenseInit(async () => {
const cached = await indexedDB.get('todos')
return cached ?? []
}),
withChangeHook((newState) => {
// Sync changes back to storage
indexedDB.set('todos', newState)
}),
)

Example 3

// Typed async init with custom default
const profile = atom<{ username: string; age: number }>(
throwAbort,
).extend(
withSuspenseInit(async () => {
const data = await fetchProfile()
return data ?? { username: 'guest', age: 0 }
}),
)

withSuspenseRetry

Creates a mixin that retries an async action when it fails coz of a suspension

This mixin wraps an async action to automatically retry it when a Promise is thrown, which indicates a suspension. It will keep retrying until the action completes successfully or throws a non-Promise error.

⚠️ Be careful with non-idempotent operations inside the action body, as they may be executed multiple times during retries. It’s recommended to carefully plan the execution logic to handle potential retries safely.

Example

const fetchUserBooks = action(async () => {
const id = user().id // ` user ` is a suspended atom
const response = await fetch(`/api/users/${id}/books `)
return response.json()
}).extend(withSuspenseRetry())