Handbook
Welcome to the awesome world of the Reatom library! š¤
This powerful tool is designed to become your go-to resource for building anything from tiny libraries to full-blown applications.
We know the drill: usually, youād have to keep reinventing the wheel with high-level patterns or depend on external libraries. Both are tough to balance perfectly for interface equality, semantic compatibility, performance, ACID principles, debugging, logging, test setup, and mocking.
To make life easier, weāve crafted the perfect building blocks (atom
and action
) and a bunch of packages on top of them.
These tools tackle the tough stuff so you can focus on being creative.
This guide will walk you through all the features of Reatom, including the core concepts, mental model, ecosystem, and infrastructure.
TL;DR
#Need a quick start? Here is a list of the key topics:
- @reatom/core: provides basic primitives to build anything. Store your state in an
atom
and your logic in anaction
. - Immutable Data: just like in React or Redux, all data changes should be immutable.
ctx
: this is essential for better debugging, simple testing, and SSR setup.- @reatom/async: will help you to handle network state smoothly.
- Helpful Packages: check out the Packages section in the sidebar for more useful tools.
- @reatom/eslint-plugin: automatically adds debug names to your code, and @reatom/logger prints helpful logs to your console.
- Template repo: will help you to get started quickly
Installation
#The core package is packed with features and has a great architecture. You can use it as is for small apps or build your own framework on top of it for larger projects.
For your convenience, weāve created a framework package suitable for most apps and developers. Basically, itās a collection of the most useful packages reexported, simplifying Reatomās use and maintenance. It shortens imports and direct dependencies, making updates easier.
Tree shaking works just fine, so donāt worry about the bundle size. Reatom development is highly focused on efficiency in this aspect.
This guide will walk you through all the main features, including installing infrastructure packages like testing and eslint-plugin. The logger package is already included in the framework, so no extra installation is needed.
The final piece of the installation script depends on your stack.
Most likely you will need @reatom/npm-react adapter package, but we also have adapters for other view frameworks.
The ānpm-ā prefix in adapter packages prevents naming collisions with ecosystem packages, as the NPM global namespace is widely used, and many common words are already taken.
A note about the ecosystem: all packages that start with ā@reatom/ā are built and maintained in the monorepo.
This approach allows us to control compatibility and stability precisely, even with minor releases. If you want to contribute a new package, feel free to follow the contributing guide.
We have a package-generator
script that will bootstrap a template for a new package.
All we need from you are the source code, tests, and documentation š
Reactivity
#Letās write some simple form code and make it reactive to boost scalability, debuggability, and testability.
Weāre using a separate state manager for this form as an example to show how it works. The benefits will get bigger as your real application grows.
Note, that all HTML elements with IDs stored in the global namespace with relative names.
So, the code above is pretty straightforward, isnāt it? However, itās already messy and has some unexpected bugs.
The first obvious issue is code duplication.
We repeat Hello...
and assign innerText
twice, and this canāt be fixed easily.
Moving it to a separate function might help, but youāll still need to call that function twice: once for initialization and once for updating.
The second significant issue is code coupling. In the code above, the update logic for the greeting is in the name update handler, but the actual data flow is inverse: the greeting depends on the name. While this might seem trivial in a small example, it can lead to confusion in real applications with complex code organization and business requirements. You might lose track of why one part of the code changes another.
Once again, we are investigating a super simple example because itās short and obvious. In this particular example there is no much sense to refactor anything, but it needed to illustrate the problems in general. That problems becomes fall your code base to a pit of failure during a time and have a huge Š²Š»ŠøŃŠ½ŠøŠµ in general, so it is important to find a pattern to reduce those problems as much as possible. So, it is the main reason for reactivity: solve state SSoT and a code coupling.
Reactive programming can address these issues by accurately describing dependent computations within each domain.
Letās refactor the code using Reatom.
We use the atom
function to wrap our changeable data:
- If you pass a primitive value to the atom, it allows the state to change.
- If you pass a
computer
function to the atom, it creates a read-only atom that automatically recomputes when dependent atoms change, but only if the computed atom has a subscription.
Now, we have the same amount of code, but it is much better organized and structured.
Plus, we have ctx
!
This context object provides powerful capabilities for debugging, testing, and many other useful features. Note? that the string names in the second atom
parameters are optional and used for debugging.
Weāll explore these advantages in more detail later.
Data consistency
#Data consistency is a critical challenge that can be difficult to manage and debug.
For instance, if your code runs in an environment that heavily uses a storage (localStorage
, for example), you might encounter a quota error when setting new data.
In such cases, users will see their input changes, but the greeting updates wonāt occur.
Although wrapping storage processing code in a try-catch
block can handle this, many developers consider these errors too rare to address in practice.
It would be great to solve these problems elegantly with a consistent pattern.
Reatom provides excellent features for maintaining data consistency. All data processing is accumulated and saved in the internal store only after completion. If an error occurs, like āCannot read property of undefined,ā all changes are discarded. This mechanism is similar to how React handles errors during the rendering process or how Redux handles errors in reducers.
This concept comes from database theory and is part of the ACID principles. Thatās why the
atom
is named so.
This transaction logic works automatically, ensuring data consistency under the hood.
You only need to keep the data immutable.
For instance, to update an array state, create a new one using the spread operator, map
, filter
, etc.
Reatom also offers the ctx.schedule
API, which separates pure computation from effects.
The benefit is that you can call ctx.schedule
anywhere, as the context propagates through all primitives and callbacks of Reatom units.
This scheduler pushes the callback to a separate queue, which is executed only after all pure computations, making your data flow safer and more manageable.
Letās apply a minor refactoring to illustrate these improvements.
Thatās it! Now, your pure computations and effects are separated. An error in the local storage logic wonāt affect the results of the atomsā computations.
Another cool feature of the schedule
API is that it returns a promise with the data from the callback.
This makes it easy to handle various data-related side effects, such as backend requests, step-by-step.
In the next section, we will introduce action
as a logic container and explore async effects.
Actions
#Letās enhance our form to create something more valuable, like a login form
Thatās it for now. The remaining part of the tutorial is a work in progress š
ā¦
Debugging
#The immutable nature of Reatom provides incredible possibilities for debugging various types of data flow, both synchronous and asynchronous. Atomsā internal data structures are specially designed for easy investigation and analysis.
One of the simplest ways to debug data states and their causes is by logging the ctx
object.
The ctx
object includes the cause
property, which holds internal representation and all meta information.
Check out this example to see it in action.
Here is an example of what you will see from logging the issuesTitlesAtom ctx
Some data is omitted for brevity, check the sandbox for the full log
As you can see, the cause
property includes all state change causes, even asynchronous ones.
But what about the empty arrays in action states?
These are lists of action calls (with payload
and params
) that only exist during a transaction and are automatically cleared to prevent memory leaks.
To view persisted actions data and explore many more features, try reatom/logger.
Additionally, you can inspect all atom and action patches by using:
Lifecycle
#Reatom is heavily inspired by the actor model, which emphasizes that each component of the system is isolated from the others. This isolation is achieved because each component has its own state and lifecycle.
This concept is applied to atoms in Reatom. We have an API allows you to create a system of components that are independent of each other and can be used in different modules with minimal setup. This is one of Reatomās main advantages over other state management libraries.
For example, you can create a data resource that depends on a backend service and will connect to the service only when the data atom is used. This is a very common scenario for frontend applications. In Reatom, you can achieve this using lifecycle hooks.
What happens here?
We want to fetch the list only when a user navigates to the relevant page and the UI subscribes to listAtom
.
This works similarly to useEffect(fetchList, [])
in React.
Since atoms represent shared state, the connection status is āone for manyā listeners, meaning an onConnect
hook triggers only for the first subscriber and not for new listeners.
This is extremely useful because you can use listAtom
in multiple components to reduce props drilling, but the side effect is requested only once.
If the user leaves the page and all subscriptions are gone, the atom is marked as unconnected, and the onConnect
hook will be called again only when a new subscription occurs.
An important aspect of atoms is that they are lazy.
This means they will only connect when they are used.
This connection is triggered by ctx.subscribe
, but the magic of Reatomās internal graph is that ctx.spy
also establishes connections.
So, if you have a main data atom, compute other atoms from it, and use these computed atoms in some components, the main atom will only connect when one of those components is mounted.
The code above will trigger the listAtom
connection and the fetchList
call as expected.
Note that the relationships between computed atoms are unidirectional. This means
filteredListAtom
depends onlistAtom
. Therefore,listAtom
is unaware offilteredListAtom
. If you useonConnect(filteredListAtom, cb)
and onlylistAtom
has a subscription, the callback will not be invoked.
When you use an adapter package like npm-react
, it utilizes ctx.subscribe
under the hood to listen to the atomās fresh state.
So, if you connect an atom with useAtom
, the atom will be connected when the component mounts.
Now, you have lazy computations and lazy effects!
This pattern allows you to control data requirements in the view layer or any other consumer module implicitly, while being explicit for data models. Thereās no need for additional start actions or similar mechanisms. This approach leads to cleaner and more scalable code, enhancing the reusability of components.
You can find many great examples in the async package docs.
Lifecycle scheme
#Reatom operates a few queues to manage mutations and pure computations, effects with different priorities to archive most intuitive and efficient execution order, with batching and transactions. We have a few nested loops, which warks the same way as ātasksā and āmicrotasksā in a browser: a parent loop tick wait all children loops to complete.
For more details on how to use the queues, refer to the ctx.schedule documentation.
- Mutations
anAction(ctx, payload);
anAtom(ctx, newState);
bach(ctx, cb);
- Computations
atom((ctx) => ctx.spy(anAtom) + 1);
reaction((ctx) => console.log(ctx.spy(anAtom)))
- Updates
anAtom.onChange(cb);
anAction.onCall(cb);
ctx.schedule(cb, 0);
- Logs
ctx.subscribe(cb);
- Rollbacks (on error)
ctx.schedule(cb, -1);
- Near effects
ctx.schedule(cb);
ctx.schedule(cb, 1);
reatomAsync(cb); onConnect(anAtom, cb);
- Late effects
ctx.subscribe(anAtom, cb);
ctx.schedule(cb, 2);
- Late effects
- Near effects
- Updates
- Computations
In other words, each next late effect will be processed after all near effects, and each near effect will be processed after all updates (and logs and rollbacks), and each update will be processed after all computations, and each computation will be processed after all mutations. So, if you will run a mutation in updates queue or effects queue the whole flow will be processed from the beginning.
Here is a visual diagram: