/reatom

Reatom - the ultimate state manager

Primary LanguageTypeScriptMIT LicenseMIT

Reatom is the ultimate logic and state manager for small widgets and huge SPAs.

Key features

  • simple and powerful abstractions. There are only three main primitives: ctx, atom, action. All other features and packages work on top of that.
  • immutable and reliable. All pure computations processed with atomicity guarantees.
  • explicit reactivity without proxies. We use the atomization pattern to achieve maximum performance
  • perfect effects management. Advanced async package allows you to describe complex async flows, including caching, retrying and automatic cancellation with native await and AbortController.
  • nice debugging experience. Each atom and action updates the ctx's immutable cause (call) stack. It helps a lot in debugging complex async flows. We also provide a logger package for that.
  • implicit DI. An isolation layer is essential to ensure complete safety when running tests and using SSR. The ctx is such an isolation layer! We offer a testing package with various helpers for mocking.
  • actor-like lifecycle hooks Learn more about self-sufficient models to achieve true modularity.
  • smallest bundle size: 2 KB gzipped With the power of base primitives, the whole ecosystem with A LOT of enterprise-level helpers takes only ~15KB. Insane!
  • the best TypeScript experience Type inference is one of the main priorities for Reatom.

The core package includes most of these features and, due to its minimal overhead, can be used in any project, from small libraries to large applications.

Adopting our well-designed helper tools allows you to efficiently handle complex tasks with minimal code. We aim to build a stable and balanced ecosystem that enhances DX and guarantees predictable maintenance for the long haul.

Simple example

Let's define input state and compute a greeting from it.

Install

npm i @reatom/core

vanilla codesandbox

react codesandbox

Simple example model

The concept is straightforward: to make a reactive variable, wrap an initial state with atom. After that, you can change the state by calling the atom as a function. Need reactive computable values? Use atom as well!

Use actions to encapsulate logic and batch atom updates.

Atom state changes should be immutable. All side effects should be placed in ctx.schedule

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)
  })
})

Simple example context

What is ctx? It is Reatom's most powerful feature. As the first argument in all Reatom functions, it provides enterprise-level capabilities with just three extra characters.

The context should be set up once for the entire application. However, it can be set up multiple times if isolation is needed, such as in server-side rendering (SSR) or testing.

import { createCtx } from '@reatom/core'

const ctx = createCtx()

Simple example view

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 a detailed explanation of fundamental principles and features.

Do you use React.js? Check out npm-react package!

Advanced example

The core package is highly effective on its own and can be used as a simple, feature-rich solution for state and logic management. Continue reading this guide if you want to tackle more complex system logic with advanced libraries for further optimizations and UX improvements.

This example illustrates a real-world scenario that highlights the complexity of interactive UIs. It features a simple search input with debouncing and autocomplete, using the GitHub API to fetch issues based on a query.

The GitHub API has rate limits, so we must minimize the number of requests and retry them if we reach the limit. Additionally, let's cancel all previous requests if a new one is made. It helps to avoid race conditions, where an earlier request resolves after a later one.

Install framework

npm i @reatom/framework @reatom/npm-react

codesandbox

Advanced example description

In this example, we will use the @reatom/core, @reatom/async and @reatom/hooks from the meta @reatom/framework package. It simplifies imports and dependencies management.

reatomAsync is a simple decorator that wraps your async function and adds extra actions and atoms to track the async execution statuses.

withDataAtom adds the dataAtom property, which holds the latest result of the effect

withCache adds a middleware function that prevents unnecessary calls by caching results based on the identity of the passed arguments (a classic cache)

withAbort defines a concurrent request abort strategy by using ctx.controller (AbortController) from reatomAsync.

Our solution for handling rate limits is based on withRetry and onReject

sleep is a built-in debounce alternative from lodash

Take a look at a tiny utils package. It contains the most popular helpers you might need.

onUpdate is a hook that connects to the atom and calls the passed callback on every atom update.

Advanced example model

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)

Advanced example view

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 logic definition consists of only about 15 lines of code and is entirely independent from the view part (React in our case). It makes it easy to test. Imagine the line count in other libraries! The most impressive part is that the overhead is less than 4KB (gzip). Amazing, right? On top of that, you’re not limited to network cache. Reatom is powerful and expressive enough to manage any state.

Please take a look at the tutorial to get the most out of Reatom and its ecosystem. If you're looking for a lightweight solution, check out the core package documentation. Additionally, we offer a testing package for your convenience!

Roadmap

  • Finish forms package.
  • Finish persist, improve url package.
  • Add adapters for the most popular ui frameworks: react, angular, vue, svelte, solid.
  • Port some components logic from reakit.io to make it fast, light and portable.
  • Add the ability to make async transactions and elaborate optimistic-ui patterns.

FAQ

Why not X?

Redux is fantastic, and Reatom draws significant inspiration from it. The principles of immutability, separating computations, and managing effects are excellent architectural design principles. However, additional capabilities are often needed when building large applications or describing small features. Some limitations are challenging to address, such as batching, O(n) complexity, and non-inspectable selectors that break atomicity. Others are just difficult to improve. And boilerplate, of course. The difference is significant. Reatom resolves these problems while offering many more features within a similar bundle size.

MobX adds a large bundle size, making it less suitable for small widgets, whereas Reatom is universal. Additionally, MobX uses mutability and implicit reactivity, which can be helpful for simple scenarios but might be unclear and difficult to debug in more complex cases. MobX lacks distinct concepts like actions, events, or effects to describe dependent effect sequences in an FRP style. Furthermore, as highlighted in this example, it does not support atomicity.

Effector is quite opinionated. It lacks first-class support for lazy reactive computations, and all connections are always hot. While this can be more predictable, it is certainly not optimal. Effector's hot connections make it unfriendly for factory creation, which prevents the use of atomization patterns necessary for efficient immutability handling. Additionally, Effector's bundle size is 2-3 times more significant with worse performance.

Zustand, nanostores, xstate, and many other state managers do not offer the same exceptional combination of type inference, features, bundle size, and performance that Reatom provides.

Why immutability?

Immutable data is more predictable and easier to debug than mutable states and their wrappers. Reatom is specifically designed to focus on simple debugging of asynchronous chains and offers patterns to achieve excellent performance.

What LTS policy is used and what about bus factor?

Reatom is built for the long haul. We dropped our first Long Term Support (LTS) version (v1) in December 2019. In 2022, we introduced breaking changes with a new LTS (v3) version. Don't worry — we've got you covered with this Migration guide. We're not stopping our three years of solid support — it's ongoing with our adapter package. We hope this proves how committed we are to our users.

Right now, our dev team consists of four people: @artalar and @krulod handle the core features, while @BANOnotIT and @Akiyamka take care of documentation and issue management. We also have many contributors working on different packages.

What build target and browser support?

All our packages are set up using Browserslist's "last 1 year" query. To support older environments, you must handle the transpilation by yourself. Our builds come in two output formats: CJS (exports.require, main) and ESM (exports.default, module). For more details, check out the package.json file.

How performant Reatom is?

Check out this benchmark for complex computations across different state managers. Remember that Reatom uses immutable data structures, operates in a separate context (DI-like), and maintains atomicity. That means the Reatom test covers more features than other state manager tests. Still, Reatom performs faster than MobX for mid-range numbers, which is pretty impressive.

Also, remember to check out our atomization guide.

Limitations

No software is perfect, and Reatom is no exception. Here are some limitations you should be aware of:

  • Immutable Data: While immutable data structures are great, they can impact performance. In critical situations, think carefully about your data structures. The good news is you don't have to use normalization.
  • Laziness: Laziness is less obvious sometimes and might lead to missed updates. However, debugging a missing update is straightforward and often easier than dealing with hot observables' memory leaks and performance issues. We also have hooks for hot linking.
  • Error Handling: Currently, you can't subscribe to errors from any dependency, but we're working on it. In reatomAsync, passed effects are wrapped in an error handler, allowing you to manage errors, but you need to wrap them explicitly.
  • Asynchronous Transactions: Asynchronous transactions are not supported yet, but they're in the works. This feature will simplify building optimistic UIs and improve UX significantly.
  • Ecosystem and Utilities: While we have many utilities and a growing ecosystem, our goal is to provide well-designed logic primitives. Reatom sits between a library and a framework, embracing procedural programming with minimal extra API and semantic overhead. Our defaults, such as immutability, laziness, transactions, and the separation of pure computations and effects, are designed to help you write better code.

Media

How to support the project?

https://www.patreon.com/artalar_dev

Zen

  • Good primitive is more than a framework
  • Composition beats configuration

Credits

Software development in the 2020s is tough, and we really appreciate all the contributors and free software maintainers who make our lives easier.

Special thanks to:

  • React, Redux, Effector and $mol for inspiration
  • microbundle for handling all bundling complexity
  • Quokka and uvu for incredible testing experience
  • TURBO for simple monorepo management
  • Astro for best in class combine of performance and developer experience
  • Vercel for free hosting and perfect CI/CD (preview branches are <3)