Atom Effects are a new experimental API for managing side-effects and initializing Recoil atoms. They have a variety of useful applications such as state persistence, state synchronization, managing history, logging, &c. They are defined as part of the atom definition, so each atom can specify and compose their own policies. This API is still evolving, and thus marked as
This API is currently under development and will change. Please stay tuned...
An atom effect is a function with the following definition.
Atom effects are attached to atoms via the
effects_UNSTABLE option. Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized. Atoms are initialized when they are used for the first time within a
<RecoilRoot>, but may be re-initialized again if they were unused and cleaned up. The atom effect function may return an optional cleanup handler to manage cleanup side-effects.
Atom families also support parameterized or non-parameterized effects:
Atom effects could mostly be implemented via React
useEffect(). However, the set of atoms are created outside of a React context, and it can be difficult to manage effects from within React components, particularly for dynamically created atoms. They also cannot be used to initialize the initial atom value or be used with server-side rendering. Using atom effects also co-locates the effects with the atom definitions.
Snapshot hooks API can also monitor atom state changes and the
initializeState prop in
<RecoilRoot> can initialize values for initial render. However, these APIs monitor all state changes and can be awkward to manage dynamic atoms, particularly atom families. With atom effects, the side-effect can be defined per-atom alongside the atom definition and multiple policies can be easily composed.
A simple example of using atom effects are for logging a specific atom's state changes.
A more complex example of logging might maintain a history of changes. This example provides an effect which maintains a history queue of state changes with callback handlers that undo that particular change:
It can be useful to use atoms as a local cached value of some other state such as a remote database, local storage, &c. You could set the default value of an atom using the
default property with a selector to get the store's value. However, that is only a one-time lookup; if the store's value changes the atom value will not change. With effects, we can subscribe to the store and update the atom's value whenever the store changes. Calling
setSelf() from the effect will initialize the atom to that value and will be used for the initial render. If the atom is reset, it will revert to the
default value, not the initialized value.
We can also bi-directionally sync atom values with remote storage so changes on the server update the atom value and changes in the local atom are written back to the server. The effect will not call the
onSet() handler when changed via that effect's
setSelf() to help avoid feedback loops.
Atom effects can be used to persist atom state with browser local storage.
localStorage is synchronous, so we can retrieve the data directly without
await or a
Note that the following examples are simplified for illustrative purposes and do not cover all cases.
Below we will use
localForage as an example of an asynchronous store.
By synchronously calling
setSelf() with a
Promise, you'll be able to wrap the components inside of the
<RecoilRoot/> with a
<Suspense/> component to show a fallback while waiting for
Recoil to load the persisted values.
<Suspense> will show a fallback until the
Promise provided to
setSelf() resolves. If the atom is set to a value before the
Promise resolves then the initialized value will be ignored.
Note that if the
atoms later are "reset", they will revert to their default value, and not the initialized value.
With this approach, you can asynchronously call
setSelf() when the value is available. Unlike initializing to a
Promise, the atom's default value will be used initially, so
<Suspense> will not show a fallback unless the atom's default is a
Promise or async selector. If the atom is set to a value before the
setSelf() is called, then it will be overwritten by the
setSelf(). This approach isn't just limited to
await, but for any asynchronous usage of
setSelf(), such as
What if you change the format for an atom? Loading a page with the new format with a
localStorage based on the old format could cause a problem. You could build effects to handle restoring and validating the value in a type safe way:
What if the key used to persist the value changes? Or what used to be persisted using one key now uses several? Or vice versa? That can also be handled in a type-safe way:
Atom state can also be persisted and synced with the browser URL history. This can be useful to have state changes update the current URL so it can be saved or shared with others to restore that state. It can also be integrated with the browser history to leverage the browser forward/back buttons. Examples or a library to provide this type of persistence are coming soon...