Recoil 0.0.10
Recoil 0.0.9 and 0.0.10 is being released with some bug fixes, TypeScript support, and a new API for Recoil Snapshots to observe, inspect, and manage global Recoil atom state. Thanks again to everyone who helped make this possible and stay tuned for more exciting developments coming soon!
#
Bug Fixes- Fixes for Server Side Rendering, though we do not officially support it yet. (#233, #220, #284) - Thanks @fyber-LJX, @Chrischuck, and @aulneau
- Fix selectors recording dependency subscriptions in some cases (#296) - Thanks @drarmstr
- Fix updaters in
useRecoilCallback()
getting current state (#260) - Thanks @drarmstr - Fix error messages when throwing certain errors in the open-source build. (#199) - Thanks @jonthomp
- Reduce Flow errors for open-source builds (#308) - Thanks @Komalov
#
Improvements- Throw error with meaningful message if user doesn't use an atom or selector with most Recoil hooks (#205) - Thanks @alexandrzavalii
- Improve testing (#321, #318, #294, #262, #295) - Thanks @aaronabramov, @Komalov, @mondaychen, @drarmstr, and @tyler-mitchell
- Improve open-source build (#249, #203, #33) - Thanks to @tony-go, @acutmore, and @jaredpalmer
#
TypeScript supportTypeScript support is being rolled into the Recoil GitHub repository instead of DefinitelyTyped
to help better keep it in sync with the API. (#292 & #339) - Thanks @csantos42
Recoil Snapshots
#312, #311, #310, #309, #260, #259, #258, #257, #256 - Thanks @drarmstr and the rest of the team
We are introducing the concept of a Snapshot
to Recoil. A Snapshot
is an immutable snapshot of the state of Recoil atoms. This is intended to standardize the API for observing, inspecting, and managing global Recoil state and derived state. It’s useful for dev tools, global state synchronization, history, and navigation.
#
API#
Reading SnapshotsThe Snapshot
class exposes the following methods for getting the values of individual Recoil atoms and selectors:
class Snapshot { getLoadable: <T>(RecoilValue<T>) => Loadable<T>; getPromise: <T>(RecoilValue<T>) => Promise<T>; ...}
Snapshots are read-only with respect to atom state. They can be used to read atom state and evaluate selector derived state. For asynchronous selectors, the getPromise()
method can be used to wait for the evaluated value so you can see what the selector value would be based on the static atom state.
#
Transforming SnapshotsThere are cases where you may wish to mutate a snapshot. While snapshots are immutable, they have methods to map themselves with a set of transformations to a new immutable snapshot. The map methods take a callback that is passed a MutableSnapshot
, which is mutated throughout the callback and will ultimately become the new snapshot returned by the mapping operation.
class Snapshot { ... map: (MutableSnapshot => void) => Snapshot; asyncMap: (MutableSnapshot => Promise<void>) => Promise<Snapshot>;}
class MutableSnapshot { set: <T>(RecoilState<T>, T | DefaultValue | (T => T | DefaultValue)) => void; reset: <T>(RecoilState<T>) => void;}
Notice that set()
and reset()
have the same signature as the callbacks provided to a writeable selector’s set()
function.
Example
const newSnapshot = snapshot.map(({set}) => set(myAtom, 42));
#
HooksRecoil has the following hooks for working with snapshots:
useRecoilSnapshot()
- Synchronous access to snapshotuseRecoilCallback()
- Asynchronous access to snapshotuseRecoilTransactionObserver()
- Subscribe to snapshots of all state updatesuseGotoRecoilSnapshot()
- Update current state to match snapshot
#
useRecoilSnapshot()function useRecoilSnapshot(): Snapshot
You can use this hook to synchronously obtain a snapshot to the current state while rendering a component. While conceptually simple, this hook will subscribe any component that uses it to any Recoil state change so it always renders with a snapshot of the current state. Therefore, be careful using this hook. One example when you may want to use it is for supporting server-side rendering where you need to synchronously have the state with the first render. In the future, we may provide the ability to debounce for performance.
Example
Define a <LinkToNewState>
component that renders an <a>
anchor with an href
based on the current state with a mutation applied. In this example uriFromSnapshot()
is some user-defined function which encodes the current state in the URI which can be restored when loading the page.
function LinkToNewState() { const snapshot = useRecoilSnapshot(); const newSnapshot = snapshot.map(({set}) => set(myAtom, 42)); return <a href={uriFromSnapshot(newSnapshot)}>Click Me!</a>;}
This is a simplified example. We have a helper like this for generating links in our browser history persistence library coming soon which is more extensible and optimized. For example, it will hijack the click handler to update local state without needing to go through the browser history.
#
useRecoilCallback()type CallbackInterface = { snapshot: Snapshot, gotoSnapshot: Snapshot => void, set: <T>(RecoilState<T>, (T => T) | T) => void, reset: <T>(RecoilState<T>) => void,};
function useRecoilCallback<Args, Return>( callback: CallbackInterface => (...Args) => ReturnValue, deps?: $ReadOnlyArray<mixed>,): (...Args) => ReturnValue
The useRecoilCallback()
hook is similar to the React useCallback()
hook for producing a callback function. But, instead of just providing an input callback function you wrap it with a function providing a callback interface parameter that gives you access to a Snapshot
and set()
/reset()
callbacks to update the current global state. The provided Snapshot
represents the state when the callback is called, not when the callback function was originally created.
NOTE: This is a slight breaking change in the API, but we are still on version 0.0.x
of Recoil and haven’t fully started semantic versioning yet.
useRecoilCallback()
also takes an optional deps
array parameter for controlling memoization. You can extend the react-hooks/exhaustive-deps
lint rule for ensuring this is properly used.
Some motivations for using useRecoilCallback()
:
Asynchronously use Recoil state without subscribing a React component to re-render if the atom or selector is updated.
Deferring expensive lookups to an async action that you don't want to do at render-time.
Performing side-effects where you would like to also read or write to Recoil state.
Dynamically updating an atom or selector where we may not know at render-time which atom or selector we will want to update, so we can't use
useSetRecoilState()
.Pre-fetching before rendering
Example
Button component which will evaluate an expensive selector when clicked on.
function ShowDetailsButton() { const onClick = useRecoilCallback(({snapshot}) => async () => { const data = await snapshot.getPromise(expensiveQuery); showPopup(data); });
return <button onClick={onClick}>Show Details</button>;}
#
useRecoilTransactionObserver()function useRecoilTransactionObserver_UNSTABLE(({ snapshot: Snapshot, previousSnapshot: Snapshot,}) => void)
This hook subscribes a callback to be executed every time there is a change to Recoil atom state. Multiple updates may be batched together in a single transaction. This hook is great for persisting state changes, dev tools, building history, &c. In the future, we may allow the ability to subscribe to specific conditions or provide debouncing for performance.
Debug Observer Example
function DebugObserver() { useRecoilTransactionObserver_UNSTABLE(({snapshot}) => { window.myDebugState = { a: snapshot.getLoadable(atomA).contents, b: snapshot.getLoadable(atomB).contents, }; }); return null;}
#
useGotoRecoilState()function useGotoRecoilSnapshot(): Snapshot => void
This hook returns a callback which takes a Snapshot
as a parameter and will update the current <RecoilRoot>
state to match this atom state.
Time Travel Example
Example list of history of state changes with the ability to go back and restore previous state.
function TimeTravelObserver() { const [snapshots, setSnapshots] = useState([]);
useRecoilTransactionObserver_UNSTABLE(({snapshot}) => { setSnapshots([...snapshots, snapshot]); });
const gotoSnapshot = useGotoRecoilSnapshot();
return ( <ol> {snapshots.map((snapshot, i) => ( <li key={i}> Snapshot {i} <button onClick={() => gotoSnapshot(snapshot)}> Restore </button> </li> )} </ol> );}
#
State InitializationThe <RecoilRoot>
component also has an initializeState
prop which can be used to initialize the atom state. This prop takes a function with a MutableSnapshot
parameter that can be used to setup the initial atom state. This can be helpful for loading persisted state when you know all atoms in advance. It can be useful for server-side rendering where the state should be setup synchronously for the first render.
Example
function MyApp() { return ( <RecoilRoot initializeState={({set}) => { for (const [atom, value] of atoms) { set(atom, value); } }} > <AppContents /> </RecoilRoot> );}
#
What’s Next?Snapshots allow us to observe and synchronize the global state. But, what if we want a more granular and composable system to work with individual atoms? We’re working on the concept of Atom Effects for observing and dealing with side-effects at the atom level. This will make it easier to persist state or bi-directionally sync with mutable storage. Think of synchronizing state with the browser URI history, browser local storage, RESTful APIs, &c. Coming soon!
The Snapshot
API introduced here allows us to inspect the current state for individual atoms and selectors. We’ll be expanding the API to be able to inspect the set of available nodes and explore the data-flow graph structure. This will be powerful for building dev tools. Stay tuned!
And, of course, we still have exiciting support for React Concurrent Mode and improved speed, scalability, and memory management in the works.