Skip to content

Instantly share code, notes, and snippets.

@Guria
Last active December 11, 2025 13:44
Show Gist options
  • Select an option

  • Save Guria/2ba7e40191ed94187e280e86639da011 to your computer and use it in GitHub Desktop.

Select an option

Save Guria/2ba7e40191ed94187e280e86639da011 to your computer and use it in GitHub Desktop.
reatom extensibility article

Reatom's Extensibility in Action: Build for Your Custom Needs

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.

The Extensibility Philosophy

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.


Case Study 1: Custom Router Bindings

The Problem

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 /#/path URLs 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.

Hash-Based Routing

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-Safe Routing

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.

The Pattern

Both examples follow the same pattern:

  1. Identify the primitiveurlAtom is the bridge between routing logic and the browser
  2. Find the hook pointurlAtom.sync controls writes, urlAtom.set controls reads, withChangeHook reacts to changes
  3. 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.


Case Study 2: Custom Form Extensions

The Problem and Use Case

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:

Collapsible Card with form

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:

  1. 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.
  2. 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
  3. Auto-collapse on reset — When the form resets (after save or cancel), the card should collapse back to summary view The built-in resetOnSubmit option 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

Inline Solution First

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.

Building withEnhancedForm

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>

Integrating with UI Patterns

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 Pattern

The form example follows the same extensibility pattern as routing:

  1. Identify the primitive — The form is built on atoms (form.fields, form.focus) and actions (form.submit, form.reset)
  2. Find the hook pointgetCalls to react to action calls, addCallHook to trigger side effects, withChangeHook for state changes
  3. Inject feature-specific behaviorsubmitIfDirty logic, reset-with-new-values, UI synchronization

Start with inline effects to solve immediate needs. Extract extensions when patterns repeat.


Generalizing the Pattern

Both case studies demonstrate the same three-step approach:

  1. Identify the primitive. Whether it's urlAtom for routing, a form's submit action, or any other atom/action in your app, find the piece that needs customization.

  2. Find the hook point. Reatom provides several:

    • atom.extend(withChangeHook(...)) — React to state changes
    • action.extend(withCallHook(...)) or addCallHook(action, ...) — React to action calls
    • getCalls(action) — Check if an action was called in the current transaction
    • Direct property overrides like urlAtom.sync.set(...) — Replace built-in behavior entirely
  3. 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.


Conclusion

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:

@Guria
Copy link
Author

Guria commented Dec 11, 2025

Collapsible Card with form

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment