Reatom v1000 just left alpha with a bold promise — "state management that grows with you." But what does that actually mean when you hit the edge cases that every real application eventually encounters?
Many developers, including myself, started using Reatom during the alpha stage because it was already delivering significant value in production. The patterns I'll show you here aren't theoretical — they've been battle-tested in real projects. If you haven't seen the introductory article, it covers the basics well. This article goes deeper: we'll explore how Reatom's extensibility lets you bend the library to your domain instead of the other way around.
The core thesis is simple: Reatom's power comes from a small set of primitives — atom, action, computed, effect, and extensions — that you can override, hook into, and compose for your specific needs. Let's see what that looks like in practice.
Most state management libraries try to anticipate every scenario. They add configuration flags, special modes, and escape hatches. Reatom takes a different approach: keep the core minimal, expose the internals, and trust developers to compose what they need.
Three ideas drive this design:
Primitives outperform frameworks. Instead of a monolithic routing system or form library, Reatom gives you building blocks. The built-in router is just atoms and actions under the hood — the same ones you'd write yourself.
Built-in features aren't black boxes. When you use reatomRoute or reatomForm, you're not locked into their behavior. They expose hooks like urlAtom.sync, form.submit.onFulfill, and withChangeHook that let you intercept or replace any behavior.
Extensions compose cleanly. The atom.extend(...) pattern lets you stack behaviors without inheritance hierarchies or complex configuration objects.
Let's see how this plays out in two real scenarios: custom routing and custom form behavior.
Reatom's built-in routing uses the History API. The urlAtom reads from window.location and syncs changes via pushState/replaceState. This works great for most SPAs, but not all environments play nice with the History API:
- Hash-based routing — Some apps need
/#/pathURLs for static hosting, legacy requirements, or Electron compatibility - Storybook isolation — When rendering components in Storybook, internal routing should work for previewing different states, but the iframe URL must stay unchanged. Otherwise, Storybook's navigation and addon panel get confused
Both scenarios require changing how urlAtom talks to the browser — without touching the routing logic itself.
The idea is straightforward: instead of reading from window.location.pathname, read from window.location.hash. Instead of pushing to history, update the hash.
Here's the complete solution:
import { reatomRoute, urlAtom, onEvent } from '@reatom/core'
// Convert hash to a URL object that the router understands
const hashToPath = () =>
new URL([window.origin, window.location.hash.replace(/^#\//, '')].join('/'))
// Utility for creating hash-based hrefs
const pathToHash = (path: string) => `#${path}`
// Set initial value (disables default History API init)
urlAtom.set(hashToPath())
// Subscribe to hash changes
onEvent(window, 'hashchange', () => urlAtom.syncFromSource(hashToPath(), true))
// Override sync to write to hash instead of history
urlAtom.sync.set(() => (url, replace) => {
const path = url.href.replace(window.origin, '')
requestAnimationFrame(() => {
if (replace) {
history.replaceState({}, '', `#${path}`)
} else {
history.pushState({}, '', `#${path}`)
}
})
})That's it. Your routes now work with hash-based URLs. The reatomRoute helper doesn't care where the URL comes from — it just consumes urlAtom. By changing how urlAtom reads and writes, you've adapted routing to a completely different environment.
You can use routes exactly as before:
const productRoute = reatomRoute({
path: ':id',
search: v.object({ tab: v.string() }),
})
// In your component
<a href={pathToHash(productRoute.path({ id: '42', tab: 'details' }))}>
View Product
</a>Storybook renders your components in an iframe. If your components use routing, clicking links will change the iframe's URL — which breaks Storybook's expectations about what story is being displayed.
The fix is even simpler than hash routing:
import { urlAtom, withChangeHook, noop } from '@reatom/core'
// Capture the original iframe URL
const originalHref = window.location.href
// Disable the sync that would normally push to history
urlAtom.sync.set(() => noop)
// Whenever urlAtom changes, restore the original URL
urlAtom.extend(withChangeHook(() => {
window.history.replaceState({}, '', originalHref)
}))Now clicking links in your story updates the routing state (so your components respond correctly), but the iframe URL stays fixed. Storybook remains happy, and you can preview all your route-dependent UI states.
Both examples follow the same pattern:
- Identify the primitive —
urlAtomis the bridge between routing logic and the browser - Find the hook point —
urlAtom.synccontrols writes,urlAtom.setcontrols reads,withChangeHookreacts to changes - Inject environment-specific behavior — Hash navigation, URL preservation, or anything else your environment requires
This pattern applies to SSR (where there's no window at all), testing (where you might want a memory-based router), or any other non-standard runtime.
Reatom's reatomForm handles the common cases well: validation, focus tracking, dirty detection, submit lifecycle. But real applications have domain-specific requirements that no library can anticipate.
Here's a pattern I encountered in a merchant dashboard:
A "Store Details" card in a merchant dashboard. In its collapsed state, the card displays the current store name as a read-only summary. When the user clicks "Edit Store Details," the card expands to reveal an inline form with editable fields.
The requirements seem simple:
- Always active submit — The submit button should never be disabled. If the user clicks "Update" without changing anything, show "No changes to submit" instead of hitting the API.
- Reset with new values — After a successful submit, the form's "initial" values must update to match what was just saved. Otherwise, the collapsed card shows stale data, or the form incorrectly flags unsaved changes next time
- Auto-collapse on reset — When the form resets (after save or cancel), the card should collapse back to summary view
The built-in
resetOnSubmitoption doesn't work here — it resets to the original initial values, not the new values from the server response.
Why not just disable the button when there are no changes?
Disabled buttons create confusion — users see a grayed-out control and wonder what's wrong. In a collapsible card pattern, someone might open the card just to verify current values, then instinctively click the primary action to "confirm" or close. An always-active button with informative feedback ("No changes to submit") is more forgiving and self-explanatory than a mysteriously disabled one
Before reaching for abstractions, let's solve the immediate problem. Since forms are built on atoms and actions, you can hook into them directly:
effect(async () => {
const calls = getCalls(myForm.submit.onFulfill)
if (isInit() || calls.length === 0) return
// Reset with current (new) values after successful submit
myForm.reset({ ...peek(() => myForm()) })
})This effect subscribes to successful form submissions. When one completes, it resets the form using its current values as the new "initial" state. The peek call reads the form state without creating a subscription, and isInit() prevents the effect from running on mount.
For a single form, this inline approach is perfectly fine. But when the pattern repeats across multiple forms, it's time to extract an extension.
Here's a reusable extension that encapsulates these behaviors:
import {
action, effect, getCalls, isInit, peek,
withChangeHook, type Action, type Computed, type Ext
} from '@reatom/core'
import type { FormEvent } from 'react'
// Define only the subset of form API we rely on
type FormLike = {
(): Record<string, unknown>
name: string
focus: Computed<{ dirty: boolean }>
submit: Action & {
onFulfill: Action
error: { set: (e: Error) => void }
}
reset: Action<[values?: Record<string, unknown>], void>
}
export type EnhancedFormExt = {
submitIfDirty: Action<[event?: FormEvent], void>
}
export const withEnhancedForm = <Target extends FormLike>({
resetWithNewValues = false,
}: { resetWithNewValues?: boolean } = {}): Ext<Target, EnhancedFormExt> => {
return (target) => {
if (resetWithNewValues) {
effect(async () => {
const calls = getCalls(target.submit.onFulfill)
if (isInit() || calls.length === 0) return
target.reset({ ...peek(() => target()) })
})
}
return {
submitIfDirty: action((event?: FormEvent) => {
event?.preventDefault()
if (target.focus().dirty) {
target.submit()
} else {
target.submit.error.set(new Error('No changes to submit'))
}
}, `${target.name}.submitIfDirty`),
}
}
}Now you can apply this to any form:
const storeForm = reatomForm({
shopName: '',
partnerId: '',
}, {
onSubmit: async (values) => api.updateStore(values),
name: 'storeForm',
}).extend(withEnhancedForm({ resetWithNewValues: true }))
// Use submitIfDirty instead of submit
<button onClick={() => storeForm.submitIfDirty()}>
Update Store Details
</button>The auto-collapse behavior is a one-liner using addCallHook:
// Inside your reatomComponent
const { open, setOpen } = useCollapsibleContext()
useEffect(() => {
return addCallHook(form.reset, () => setOpen(false))
}, [form, setOpen])Whenever form.reset is called — whether from a successful submit, a cancel button, or anywhere else — the card collapses. No imperative coordination required.
The form example follows the same extensibility pattern as routing:
- Identify the primitive — The form is built on atoms (
form.fields,form.focus) and actions (form.submit,form.reset) - Find the hook point —
getCallsto react to action calls,addCallHookto trigger side effects,withChangeHookfor state changes - Inject feature-specific behavior —
submitIfDirtylogic, reset-with-new-values, UI synchronization
Start with inline effects to solve immediate needs. Extract extensions when patterns repeat.
Both case studies demonstrate the same three-step approach:
-
Identify the primitive. Whether it's
urlAtomfor routing, a form's submit action, or any other atom/action in your app, find the piece that needs customization. -
Find the hook point. Reatom provides several:
atom.extend(withChangeHook(...))— React to state changesaction.extend(withCallHook(...))oraddCallHook(action, ...)— React to action callsgetCalls(action)— Check if an action was called in the current transaction- Direct property overrides like
urlAtom.sync.set(...)— Replace built-in behavior entirely
-
Inject your behavior. Write the environment-specific or feature-specific logic, keeping it composable so it can be reused or removed easily.
Look at your own codebase. Where do you have repetitive patterns that the library doesn't quite handle? Where are you working around limitations with manual imperative code? Those are opportunities for extensions.
Reatom v1000's design assumes you know your domain better than any library author. Instead of anticipating every edge case with configuration flags and special modes, it exposes composable primitives and trusts you to assemble what you need.
The examples in this article — hash-based routing, Storybook isolation, form reset behaviors — aren't special cases that required library changes or workarounds. They're natural applications of Reatom's extension system, using the same patterns that power the built-in features.
If you're building something complex enough that off-the-shelf solutions don't quite fit, Reatom gives you the hooks to make it fit. Start with the primitives, find the extension points, and build exactly what your application needs.
Links:

Uh oh!
There was an error while loading. Please reload this page.