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 support

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

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

There 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));

Hooks

Recoil has the following hooks for working with snapshots:

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 Initialization

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