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.

Recoil 0.0.8

Today we are releasing Recoil 0.0.8. It contains bug fixes and new features. Thanks so much to everyone who contributed to this release! It's been amazing to see so many people contribute.

Bug Fixes

  • Fixed a bug where atoms that stored self-referential structures would cause an infinite loop. (@n3tr in #153)
  • Fixed bugs affecting Server-Side Rendering. (@sbaudray in #53)
  • Fixed build system and repository syncing problems. Many people contributed to this, especially @mondaychen and including @claudiopro, @dustinsoftware, @jacques-blom, @jaredpalmer, @kentcdodds, @leushkin, and @tony-go. It remains to get Jest and Flow to behave the same between internal and OSS.

Features

TypeScript support

TypeScript definitions are now available via the DefinitelyTyped repository.

atomFamily and selectorFamily

These utilities help you create collections of related atoms or selectors, one for each value of some parameter. Instaed of manually creating a memoized function that returns an atom or selector, you can use atomFamily and selectorFamily. In the future, these utilities will also help with memory management.

The atomFamily function returns a function from some parameter to an atom, creating a new atom for each value of the parameter that is passed in. For example, suppose you wanted to store a set of coordinates {x: number, y: number} for every member of a collection identified by some ID. Then you could write:

const coordinatesForID = atomFamily<{x: number, y: number}, ID>({
key: 'coordinatesForID',
default: {x: 0, y: 0},
});

and then access that state as follows:

function MyComponent({id}) {
const [coordinates, setCoordinates] = useRecoilState(
coordinatesForID(id)
);
...
}

Each ID passed to coordinatesForID will get its own independent atom containing the coordinates. Each of these atoms has its own subscriptions, so a component that uses the state for a single ID will only be subscribed to changes to that one ID.

Similarly, selectorFamily lets you create a different selector for each value of some parameter. For example, suppose you wanted to take each of those coordinates and rotate them by 180 degrees:

const rotatedCoordinatesForID = selectorFamily<{x: number, y: number}, ID>({
key: 'rotatedCoordinatesForID',
get: id => ({get}) => {
const coordinates = get(coordinatesForID(id));
return {
x: -coordinates.x,
y: -coordinates.y,
};
}
});

Note that the get property of a selector family is a function that takes the parameter (in this case ID) and returns a function with a signature identical to the get property of a single selector. In general, this is how all of the options for atom families and selector families work.

Concurrency helpers

We've introduced selector families for controlling concurrency in async selectors:

  • waitForAll: requests all dependencies in parallel and waits for all of them to become available.
  • waitForAny: requests all dependencies in parallel and waits for any one of them to become available.
  • waitForNone: requests all dependencies in parallel but doesn't wait for any of them.
  • noWait requests a single dependency but doesn't wait for it to become available.

These can be used to retireve multiple dependencies in parallel and to write logic conditional on the status of an upstream dependency. For example, you can write a selector that conditionally provides a default value while an async process is in flight instead of propagating that loading state down to components.

constSelector and errorSelector

These selector families simply return a constant value or always throw a given error, respectively.

readOnlySelector

This simply wraps a read-write RecoilState in a read-only interface.

What's Next

We're working on improvements to the observation and persistence APIs, improved speed and memory management, and support for Concurrent Mode. Thanks so much for trying Recoil, we hope you'll stick around and see what it becomes!