artalar / reatom

Reatom - the ultimate state manager
https://reatom.dev
MIT License
969 stars 103 forks source link
flux react reactive reactive-programming reactivity reatom state state-management state-manager store

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

Key features

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

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:

Media

How to support the project?

https://www.patreon.com/artalar_dev

Zen