The origin of

Recoil originated from an application related to visual data analysis within Facebook. In the process of implementing React, the existing state management tools could not well meet the needs of the application, so Recoil was spawned.

This application has complex interactions that can be summarized as follows:

  • A number of scenarios that require shared state
  • A large number of scenarios that require derived states (calculating a new state based on some state)
  • The state can be persisted, thus restoring the current scenario through the persisted state

The problem of story

  • flexibility

Redux’s verbose code achieves its promised goal of maintaining predictability, but naturally falls short of flexibility. Status sharing cannot be easily carried out, side effect management needs to introduce various middleware, and React Suspense components cannot be well connected, as well as Concurrent mode in the future.

  • performance

Redux’s strategy of sharing data is a performance challenge in scenarios where complex operations and large numbers of components share data.

  • The code in some scenarios is not robust enough

In a derived state scenario, it is often necessary to manually dessynchronize the various states, rather than make responsive automatic updates, so it is easy to have bugs with different states

What does Recoil solve

Flexible and high-performance sharing state

Faced with the need to share the state, we can choose the following methods:

  • The React itself
  • Use Redux
  • Use the Context

Imagine a situation where you have hundreds of rectangular components displayed on the screen, and you can drag any one of them, and there’s a panel on the left that shows you where it’s being dragged in real time.

This means that the rectangle needs to maintain its own coordinates and share them with the panel. All three of the above methods have some limitations when faced with this scenario:

The React itself

React itself deals with data sharing by promoting state to the parent component, which naturally causes all child components to be rerendered if a component’s state changes. Although we can optimize this using the memo function, we still have the problem of waking up and comparing the Props to the Props. Another problem is that once another component needs to look at the shared data, it can become cumbersome to continue to promote the data.

Use Redux

In the same way, Redux is an Action that wakes up all subscribed components, even if their subscribed data has not changed, and can only prevent invalid rendering by shallow (or deep) comparisons to see if the data is consistent after the change. Performance issues are still encountered when faced with high frequency scenarios such as dragging updates.

Use the Context

Context is a perfect way to solve this problem. Each rectangular component has its own Context. Thanks to the Context mechanism, when the data of the Context changes, only the component listening to the relevant Context will be rerendered. The problem with Context is that Context can’t handle scenarios that are too dynamic. If these rectangular components are inserted dynamically, for example, by clicking a button, it means that their corresponding Context also needs to be inserted dynamically into the top-level components to share data with other components. However, because of React’s diff policy, it is also unacceptable that if a Context or any component is dynamically inserted into a top-level component, the subcomponent tree is constantly destroyed and rebuilt. Not to mention, if you’re in a scenario where you’re dealing with code-spliting, because the bottom component is related to the top component, that connection makes it very difficult to separate out the child component, for example, how do you insert the Context of the child component into the top layer after it’s asynchronously loaded.

Recoil solves this problem by building its own state tree, separate from React, that exists parallel to the component tree. The state tree consists of Atom and selectors.

The basic unit of the state tree is called Atom. An Atom represents a variable, subsable state. When the state represented by Atom changes, only the component that subscribed to Atom is rerendered, without affecting its other components.

1  const todoListState = atom({
2    key: 'todoListState',
3    default: [],
4  });
Copy the code

The derived condition

When we talk about unrobust code, we’re talking about bug-prone code, and code that manually synchronizes state can be thought of as error-prone code, and the more states you have, the more error you get. Reactive programming works well to solve this problem, such as RxJS streams, or Vue computational properties. The common feature is that when dependencies (upstream) change, derived objects (downstream) also change automatically, avoiding the need to manually maintain state synchronization, thus reducing the chance of bugs.

For this, Recoil provides a Selector. A Selector also represents a state, but that state is derived from Atom and other selectors, and when the Selector dependency changes, The Selector will calculate the new state based on the changed dependency response.

1 const todoListFilterState = atom({ 2 key: 'todoListFilterState', 3 default: 'Show All', 4 }); 5 6 const filteredTodoListState = selector({ 7 key: 'filteredTodoListState', 8 get: ({get}) => { 9 const filter = get(todoListFilterState); 10 const list = get(todoListState); 11 12 switch (filter) { 13 case 'Show Completed': 14 return list.filter((item) => item.isComplete); 15 case 'Show Uncompleted': 16 return list.filter((item) => ! item.isComplete); 17 default: 18 return list; 19} 20}, 21})Copy the code

Even more powerful is that the Selector can be asynchronous, meaning that you can get the information you want from another Atom or Selector, then initiate an asynchronous query, return the corresponding data, and then use that asynchronous Selector in your component. Similarly, asynchronous selectors will change responsively depending on the dependencies.

1 const currentUserNameQuery = selector({ 2 key: 'CurrentUserName', 3 get: async ({get}) => { 4 const response = await myDBQuery({ 5 userID: get(currentUserIDState), 6 }); 7 return response.name; 8}, 9}); 10 11 function CurrentUserInfo() { 12 const userName = useRecoilValue(currentUserNameQuery); 13 return <div>{userName}</div>; 14}Copy the code

You might wonder what’s going on underneath this code, taking advantage of the React < Suspense /> component feature, which means that < CurrentUserInfo /> components that use asynchronous selectors must be encapsulated in < Suspense />, Otherwise React will just report an error.

Although the entire pull is asynchronous under the code, with < CurrentUserInfo /> you are using the data in a synchronous manner and the asynchronous state is managed by React, which also means less mental burden and coupling. Recoil also ensures that asynchronous selectors do not have race issues, always returning the same result for the same input (dependencies), which again means that there is some caching strategy.

Observe the status of an application dimension

Another feature of Redux is that it’s easy to do time travel across the application dimension. In theory, if you use Redux to manage all the states, you simply serialize the entire Redux state tree at any given moment, saving a snapshot of the application at that moment. With this in mind, it’s easy to do some sketching, editing, time travel, sharing, etc.

To address these requirements Recoil also supports state observation of application dimensions. For example, we can serialize the state to the address bar, share it with others, and when others access it, deserialize the data from the address bar to the application, and restore the state of the application at that time. In addition, this feature makes debugging easy.

Concurrent mode

Recoil is implemented with Concurrent modes in mind, so it can be supported quickly once Concurrent modes become stable.

reference

  • Recoil: State Management for Today’s React – Dave McCabe aka @mcc_abe at @ReactEurope 2020

Author: He Yunfei