Reatom - the ultimate state manager
Reatom is a ultimate logic and state manager for small widgets and huge SPAs.
ctx
, atom
, action
, all other features and packages works on top of that.
await
and AbortController
.
ctx
is! We have the extra testing package with pack of helpers for a mocking.
The core package includes most these features and you may use it anywhere, from huge apps to even small libs, as the overhead is tiny. Also, you could reuse our carefully written helper tools to solve complex tasks in a couple lines of code. We are trying to build a stable and balanced ecosystem for perfect DX and predictable maintains even for years ahead.
Let's define input state and compute a greeting from it.
npm i @reatom/core
The concept is dumb - if you want to make a variable reactive, wrap the init state in atom
, you will alow to change the state by calling it as a function. Need reactive computed? - wrap it to atom
!
action
s needed to separate logic and batch atom updates.
All data processing should be immutable, all side-effects should be wrapped to ctx.schedule
.
What is ctx
? It is the most powerful feature of Reatom. It flows as the first argument across all Reatom functions, bringing you enterprise-grade features with only three extra symbols!
import { action, atom } from '@reatom/core'
const initState = localStorage.getItem('name') ?? ''
export const inputAtom = atom(initState)
export const greetingAtom = atom((ctx) => {
// `spy` dynamically reads the atom and subscribes to it
const input = ctx.spy(inputAtom)
return input ? `Hello, ${input}!` : ''
})
export const onSubmit = action((ctx) => {
const input = ctx.get(inputAtom)
ctx.schedule(() => {
localStorage.setItem('name', input)
})
})
The context set up once for the whole app, or multiple times if you need isolation in SSR or tests.
import { createCtx } from '@reatom/core'
const ctx = createCtx()
import { inputAtom, greetingAtom, onSubmit } from './model'
ctx.subscribe(greetingAtom, (greeting) => {
document.getElementById('greeting')!.innerText = greeting
})
document.getElementById('name').addEventListener('input', (event) => {
inputAtom(ctx, event.currentTarget.value)
})
document.getElementById('save').addEventListener('click', () => {
onSubmit(ctx)
})
Check out @reatom/core docs for detailed explanation of key principles and features.
Do you use React.js? Check out npm-react package!
The core package is already profitable and you could use only it as a most simple and featured solution for your state and logic management. If you want to solve common system logic with advanced libraries to care more optimizations and UX tasks - continue and check this small guide out.
This example is close to real life and shows the complexity of interactive UI. It is a simple search input with debouncing and autocomplete. It uses GitHub API to fetch issues by query. The limitations of this API are described in GitHub docs. We need to reduce the number of requests and retry them if we hit the limit. Also, we need to cancel all previous requests if a new one is created, to prevent race conditions - when the previous request resolves after the new one.
npm i @reatom/framework @reatom/npm-react
We will use @reatom/core, @reatom/async and @reatom/hooks packages in this example by importing it from the meta package @reatom/framework - it simplifies imports and dependencies management.
reatomAsync
is a simple decorator which wraps your async function and adds extra actions and atoms to track creating promise statuses.
withDataAtom
adds property dataAtom
which saves the last effect result and allow you to subscribe to it. withCache
enable function middleware which prevent it's extra calls based on passed arguments identity - classic cache. withAbort
allows to define concurrent requests abort strategy, by using ctx.controller
(AbortController) from reatomAsync
. withRetry
and onReject
handler help to handle temporal rate limit.
Simple sleep
helper (for debounce) gotten from utils package - it is a built-in microscopic lodash alternative for most popular and tiny helpers.
onUpdate
is a hook which link to the atom and calls passed callback on every update.
import { atom, reatomAsync, withAbort, withDataAtom, withRetry, onUpdate, sleep, withCache } from "@reatom/framework"; // prettier-ignore
import * as api from './api'
const searchAtom = atom('', 'searchAtom')
const fetchIssues = reatomAsync(async (ctx, query: string) => {
await sleep(350) // debounce
const { items } = await api.fetchIssues(query, ctx.controller)
return items
}, 'fetchIssues').pipe(
withAbort({ strategy: 'last-in-win' }),
withDataAtom([]),
withCache({ length: 50, swr: false, paramsLength: 1 }),
withRetry({
onReject(ctx, error: any, retries) {
// return delay in ms or -1 to prevent retries
return error?.message.includes('rate limit')
? 100 * Math.min(500, retries ** 2)
: -1
},
}),
)
// run fetchIssues on every searchAtom update
onUpdate(searchAtom, fetchIssues)
import { useAtom } from '@reatom/npm-react'
export const Search = () => {
const [search, setSearch] = useAtom(searchAtom)
const [issues] = useAtom(fetchIssues.dataAtom)
// you could pass a callback to `useAtom` to create a computed atom
const [isLoading] = useAtom(
(ctx) =>
// even if there are no pending requests, we need to wait for retries
// let do not show the limit error to make him think that everything is fine for a better UX
ctx.spy(fetchIssues.pendingAtom) + ctx.spy(fetchIssues.retriesAtom) > 0,
)
return (
<main>
<input
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder="Search"
/>
{isLoading && 'Loading...'}
<ul>
{issues.map(({ title }, i) => (
<li key={i}>{title}</li>
))}
</ul>
</main>
)
}
The whole logic definition is only about 15 LoC and it is not coupled to React and could be tested easily. What would the lines count be in a different library? The most impressive thing is that the overhead is less than 4KB (gzip) could you imagine?! And you are not limited to network cache, Reatom is powerful and expressive enough for describing any kind of state.
To get maximum of Reatom and the ecosystem just go to tutorial. If you need something tiny - check out the core package docs. Also, we have a package for testing!
Redux is awesome and Reatom is heavy inspired by it. Immutability, separation of computations and effects are good architecture designs principles. But there are a lot of missing features, when you trying to build something huge, or want to describe something small. Some of them is just impossible to fix, like batching, O(n) complexity or that selectors is not inspectable and breaks the atomicy. Others is really hard to improve. And boilerplate, yeah, the difference is a huge. Reatom solves all this problems and bring much more features by the almost same size.
MobX brings too big bundle to use it in a small widgets, Reatom is more universal in this case. Also, MobX has mutability and implicit reactivity, which is usefull for simple cases, but could be not obvious and hard to debug in complex cases. There is no separate thing like action / event / effect to describe some dependent effects sequences (FRP-way). There is not atomicy too.
Effector is too opinionated. There is no first-class support for lazy reactive computations and all connections are hot everytime, which is could be more predictable, but defenetly is not optimal. Effector is not friendly for fabric creation (because of it hotness), which disallow us to use atomization patterns, needed to handle immutability efficient. The bundle size is 2-3 times larger and performance is lower.
Zustand, nanostores, xstate and many other state managers have no so great combination of type inference, features, bundle size and performance, as Reatom have.
Immutable data is much predictable and better for debug, than mutable states and wrapers around that. Reatom specialy designed with focus on simple debug of async chains and have a patterns to handle greate performance.
Reatom always developed for long time usage. Our first LTS (Long Time Support) version (v1) was released in December 2019 and in 2022 we provided breaking changes less Migration guide to the new LTS (v3) version. 3 years of successful maintains is not ended, but continued in adapter package. We hope it shows and prove our responsibility.
Currently, there are four people in the development team: @artalar and @krulod are managing the core features, while @BANOnotIT and @Akiyamka help with documentation and issue management. A lot of people contributes to different packages.
All packages are configured based on Browserslist's "last 1 year" query. If you need to support older environments, you should handle transpilation yourself.
All builds have two types of output formats: CJS (exports.require
, main
) and ESM (exports.default
, module
). You can check package.json
for more details.
Here is the benchmark of complex computations for different state managers. Note that Reatom by default uses immutable data structures, works in a separate context (DI-like) and keeps atomicity, which means the Reatom test checks more features, than other state manager tests. Anyway, for the middle numbers Reatom faster than MobX which is pretty impressive.
Also, check out atomization guide.
Of course there are no software without limitations. Reatom is trying to be a silver bullet but we still have some cases which you should know about.
ctx
and connections and processes virtualizations.https://www.patreon.com/artalar_dev
Software development in 202X is hard and we really appreciate all contributors and free software maintainers, who make our life easier. Special thanks to: