DEV Community

Artyom
Artyom

Posted on

Reatom: State Management That Grows With You

The Fragmentation Problem

Modern frontend development has a familiar pattern:

  • Start with simple useState hooks
  • Need shared state? Add Context
  • Context re-renders too much? Add a state manager
  • State manager doesn't handle async well? Add a data fetching library
  • Need forms? Another library
  • Routes need data loading? Router with loaders
  • Persistence? Yet another library
  • Logging and analytics? Here we go again...

Each tool has its own mental model, API quirks, and bundle cost. The stack grows, complexity multiplies, and new team members need weeks to understand it all. Versioning and maintaining compatibility across multiple libraries becomes a nightmare.

Reatom is a different approach. One coherent system that can handle all of these concerns — from the simplest counter to the most complex enterprise data flows. Use what you need, integrate with what you have.

For fast overview, you can check out our site: https://v1000.reatom.dev/

The Zen

Four principles guide Reatom's design:

  • Primitives outperform frameworks — A few powerful building blocks beat a complex framework
  • Composition beats configuration — Stack simple pieces instead of configuring monoliths
  • Explicit specifics, implicit generics — Be clear about what matters, hide the boilerplate
  • Compatibility worth complexity — Works with your stack, not against it

These aren't marketing slogans. They're the result of six years of production use and continuous refinement, starting with the first LTS release in December 2019.

Start Simple

If signals are familiar, Reatom will feel natural:

import { atom, computed } from '@reatom/core'

const counter = atom(0)
const isEven = computed(() => counter() % 2 === 0)

// Read
console.log(counter()) // 0
console.log(isEven()) // true

// Write
counter.set(5)
counter.set((prev) => prev + 1)
Enter fullscreen mode Exit fullscreen mode

No providers, no boilerplate, no ceremony. The core is less than 3KB gzipped — smaller than some "lightweight" alternatives.

You can stop there, but here's where it gets interesting...

Grow Infinitely

The same primitives that power a counter can handle enterprise-grade complexity:

import { atom, computed, withSearchParams, withAsyncData, wrap, sleep } from '@reatom/core'

// Sync with URL search params automatically
const search = atom('', 'search').extend(withSearchParams('search'))
const page = atom(1, 'page').extend(withSearchParams('page'))

// Async computed with automatic cancellation
const searchResults = computed(async () => {
  const url = `/api/search?q=${search()}&page=${page()}`
  await wrap(sleep(300)) // The wrap provides debouncing here

  const response = await wrap(fetch(url))
  return await wrap(response.json())
}, 'searchResults').extend(withAsyncData({ initState: [] }))

// Now you have:
searchResults.ready() // loading state
searchResults.data() // the results
searchResults.error() // any errors
Enter fullscreen mode Exit fullscreen mode

What happens here:

  1. search and page atoms automatically sync with URL params
  2. The computed re-runs only when dependencies change AND when something subscribes to it (lazy!)
  3. If the user types faster than the API responds, previous requests are automatically cancelled
  4. Race conditions? Handled. Memory leaks? Impossible.

This is the same atom from the counter example — just extended.

The Extension System

Instead of a monolithic framework, Reatom provides composable extensions that stack cleanly:

const theme = atom('dark').extend(
  // Persist to localStorage
  withLocalStorage('theme'),
)

const list = atom([]).extend(
  // Memoize an equal changes
  withMemo(),
)

const unreadCount = atom(0).extend(
  // React to state changes
  withChangeHook((count) => {
    document.title = count > 0 ? `(${count}) My App` : 'My App'
  }),
  // Sync across tabs
  withBroadcastChannel('notificationsCounter'),
)
Enter fullscreen mode Exit fullscreen mode

Each extension does one thing well. Compose exactly what's needed.

Beyond State: Complete Solutions

Reatom provides full solutions for common frontend challenges:

Forms

Type-safe form management with first-class support for:

  • Field-level and form-level validation (sync and async)
  • Integration with Standard Schema validators (Zod, Valibot, etc.)
  • Dynamic field arrays with add, remove, reorder operations
  • Focus tracking, dirty detection, error management
  • No weird string paths — just objects and atoms
  • Extremely optimized and performant

https://v1000.reatom.dev/start/forms/

Routing

Type-safe routing with automatic lifecycle management:

  • Parameter validation and transformation
  • Data loading with automatic cancellation on navigation
  • Nested routes with shared layouts
  • Search parameter handling
  • Isomorphic cross-framework support
  • Factory pattern — state created in route loaders is automatically garbage collected on navigation, solving the "global state cleanup" problem

https://v1000.reatom.dev/start/routing/

Persistence

A ton of built-in storage adapters with general advanced features:

  • localStorage, sessionStorage, BroadcastChannel, IndexedDB, Cookie and Cookie Store
  • Integration with Standard Schema validators (Zod, Valibot, etc.)
  • Version migrations for data format changes
  • TTL (time-to-live) support
  • Graceful fallback to memory storage when unavailable

https://v1000.reatom.dev/handbook/persist/

Deep Technical Advantages

Cause Tracking

Reatom emulates TC39 AsyncContext and gets a TON of benefits from it:

The last feature is a game changer for complex async flows. You can easily inspect the cause of your concurrent operations and reduce debugging time from hours to minutes.

Performance

Reatom has the best possible performance given its feature set. The more complex the application, the faster Reatom performs compared to alternatives.

Benchmarks for complex computations show Reatom outperforming MobX in moderately sized dependency graphs — impressive given that Reatom uses immutable data structures and operates in a separate async context, features that bring significant benefits but typically add overhead.

Explicit Reactivity, No Proxies

Proxy-based reactivity can be convenient for simple cases but becomes harder to trace in large codebases. With Reatom, hover over any property for a type hint — it's always clear what's reactive.

Reatom encourages atomization — transforming backend DTOs into application models where mutable properties become atoms:

// Backend DTO
type UserDto = { id: string; name: string }

// Application model with explicit reactivity
type User = { id: string; name: Atom<string> }

const userModel = { id: dto.id, name: atom(dto.name) }

// Now it's always clear:
userModel.id // static value — no reactivity
userModel.name() // reactive — tracks changes
Enter fullscreen mode Exit fullscreen mode

This pattern gives you fine-grained control: define reactive elements precisely where needed, avoid the uncontrolled creation of observers, and never need toJS-like conversions to inspect or pass data.

Less complexity with atomization

In immutable state managers, updating one item in a list requires recreating the entire array — O(n) complexity. Reatom's atomization pattern provides O(1) updates:

// Traditional immutable approach: O(n)
const updateName = (state, idx, name) => ({
  ...state,
  users: state.users.map((u, i) => (i === idx ? { ...u, name } : u)),
})

// Reatom with atomization: O(1)
users()[idx].name.set(newName)
Enter fullscreen mode Exit fullscreen mode

For lists with thousands of items, this difference matters.

Framework Agnostic, Ecosystem Friendly

Right now Reatom works best with React (https://v1000.reatom.dev/reference/react), but we already have draft packages for Vue, Preact, Solid, and Lit, with plans for Svelte and Angular (signals). You can also bind any library using reatomObservable.

Write your state and effects once, get a reusable model for complex tests and any view framework. Perfect for modularity in general, and microfrontends especially.

Reatom isn't a walled garden. It plays well with the native web APIs and the entire npm ecosystem: use Zod for validation, any fetch wrapper for HTTP, use built-in AbortController's, bind any UI library for components. Adopt Reatom incrementally in existing projects — no need to rewrite everything at once.

Try It

npm install @reatom/core @reatom/react
Enter fullscreen mode Exit fullscreen mode

Start with atoms. Add extensions as needed. Scale to any complexity.


Links:

Top comments (8)

Collapse
 
sylvan008 profile image
Oleg

Using reatom v3 to start the project was the best solution.
A year and a half in production and no serious complaints. Some of the tasks that we solved with reatom are even scary to imagine how we would have to implement them on query and react.
I hope to be able to migrate to the reatom v1000 in the next few months.

Collapse
 
__97a6a06c2ed93436 profile image
Mohammed Alghazali

This is the best thing I have ever seen for React ecosystem

Collapse
 
__71dcf3ee profile image
Евгений Некрасов

I haven't tried this state manager yet, but it's already made quite a splash in the CIS community! I'll definitely give it a try

Collapse
 
y0na24 profile image
Матвей Клёнов

Great tool! Thank you for your effort :)

Collapse
 
__a5e4dc9cc5eb6 profile image
Антон Жуков • Edited

Amazing! @artalar, where is the hash-router and memory-router?

github.com/reatom/reatom/issues/1214

Collapse
 
artalar profile image
Artyom

All our routing features are based on urlAtom, which contains the current URL and may or may not sync it with the browser location or node req.url (for example). However, we lack documentation, yes, we will add some descriptions.

Collapse
 
guria profile image
Aleksei Gurianov

I will share an article soon. keep tuned

Collapse
 
guria profile image
Aleksei Gurianov

dev.to/guria/reatom-extensibility-...

TLDR

import { urlAtom, onEvent } from '@reatom/url'

const hashToPath = () =>
  new URL([window.origin, window.location.hash.replace(/^#\//, '')].join('/'))

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({}, '', pathToHash(path))
    } else {
      history.pushState({}, '', pathToHash(path))
    }
  })
})
Enter fullscreen mode Exit fullscreen mode