The introduction

When it comes to the React state manager library, everyone is used to using Redux, so I’ll introduce you at the beginningRecoilBefore that, let’s take a look at the usage scenario and compare the design ideas and implementation methods of the two libraries.

Project Function Description

  1. On the left is a list of all items, and new items can be added.
  2. The middle canvas shows all items. Item can be dragged, and the property on the right is updated in real time as you drag it.
  3. By modifying the property pane information on the right, the Item can be updated in real time in the canvas.

Redux implementation

To store all items, we typically define an array to store:

import { createStore } from 'redux'
const initialState = { items: []};function todoApp(state = initialState, action) {
  switch (action.type) {
    case 'ADD':
      return { items: [...state.items, action.preload] }
    default:
      return state
  }}
export cosnt store = createStore(todoApp)
Copy the code

Then render all items with a map:

function Canvas(props) {
  const { items } = props;
  return (
    <div>
      {items.map(item => {
        return <Item key={item.id} item={item} />
      })}
    </div>
  );
}
Copy the code

What’s wrong with this approach?

Adding an Item and updating any Item triggers updating all Item components (for what reason). And for the above items, dragging Item is such a high-frequency operation. Therefore, if we can achieve that only the dragged Item is updated, and other items are not updated, it will be helpful to improve the performance.

Recoil implementation

To ensure that the update of a single Item does not affect other items, the state of each Item should be independent.

This is Recoil’s concept of state management. Recoil advocates the independent definition and management of states. Therefore, in the above scenario, we can define each Item’s state. In this way, when a single Item moves, only the state of this Item changes and thus triggers the update.

In Recoil, we use the Atom function to define a state. In this way, we can define a separate state for each Item.

export const itemState = atom({
  key: 'itemState'.default: {}})Copy the code

It is then used within the Item component

import { useRecoilState } from 'recoil';
import { itemState } from '.. /state';
function Item() {
  const [item, setItem] = useRecoilState(itemState);
  // other
  return (
    <div>{{ item.title }}</div>
  );
}
Copy the code

The above implementation is to explicitly define the state of an Item. In the above scenario, items can be added and deleted, so we need to have the ability to dynamically create and delete states.

Therefore, we can provide a method to obtain the state of the Item based on its ID and implement caching.

const stateCache = {};
export const getItemState = id= > {
  if(! stateCache[id]) { stateCache[id] = atom({key: `item-${id}`.default: {}})}return stateCache[id]
}
Copy the code

For use in components:

import { useRecoilState } from 'recoil';
import { getItemState } from '.. /state';
function Item({ id }) {
  const [item, setItem] = useRecoilState(getItemState(id));
  // other
  return (
    <div>{{ item.title }}</div>
  );
}
Copy the code

In this way, each Item is an independent state, and any changes to it will not affect other items.

State sets and state caches

Our getItemState already implements the collection and caching of states. Recoil offers a tool for natively called atomFamily. There is no need for us to do another encapsulation, and better memory management from the bottom up.

export const itemFamily = atomFamily({
  key: 'itemFamily'.default: id= >{}})Copy the code

This is the core concept of Recoil’s state management, which is to separate separate states and manage them independently, avoiding unnecessary rendering and making it easy to expand and manage. Redux is a centralized management of state, with basically only one global state for the entire application.

So far, I’ve introduced you to Recoil’s state management concept of separate definitions, as well as state collection and state caching capabilities. Next, we’ll look at Recoil in more detail.

Recoil

Community activity

Let’s take a look at Recoil’s current use in the community. (As of 2020.10.30)

Weekly download curve of NPM packages:

Github repository start number:

We can see that the growth trend is very fast, the first official release of 2020.5.30.

From the above two sets of data, it can be seen that as a new tool, it still has a high level of attention in the community.

Facebook’s latest launch

Recoil was launched by Facebook (official lineage). Officially, it’s still experimental, but some of Facebook’s internal products are already in production.

Fast update and iteration

The first version (0.0.8) was released on May 30, 2020, and the latest version (0.0.13) has been released almost every month.

Hook Only

All apis are provided in hook mode and are only supported in Function Component. Not supported in Class Component. So, before you decide whether to introduce Recoil into your project, you need to double check that all state-sharing components in your app are Function Components. If not, you should abandon Recoil. However, with the advance and wide application of React Hook, this should be a trend of React. In principle, any Class Component can be implemented as Function Component.

Three core features

Minimal and Reactish

Reocil advocates decentralized state management (similar to MOBx), allowing us to individually define individual sub-states in our applications, making state management more efficient and scalable. More on the atomized definition of state later.

Recoil was born entirely for React. React uses the same style as React, with no new syntax to learn. All of the API is provided as React, which is an understandable extension to React.

Data-Flow Graph

State sharing has a data flow diagram, a closed loop from shared state to component and component to shared state. A component subscribes to the state and forms a data flow from the state to the component, and a component updates the state and forms a data flow from the component to the state.

In Recoil’s data flow diagram, derived state and asynchronous state can be easily introduced without secondary encapsulation. Redux has no asynchronous data flow (Redux-Thunk, Redux-Saga are packages based on synchronous data flow).

Cross-App Observation

Recoil provides the Snapshot status Snapshot function. It allows us to persist state, observe, monitor, and manage state changes. Personally, in fact, this is a supplement to the lack of decentralized state management, like Redux state centralized management is easy to implement this function.

supportConcurrent Mode

In the current release, there are some experimental apis to support React. Will work with React to efficiently support Concurrent Models. Currently, no other state management libraries support Concurrent Mode

What is Concurrent Mode

React complex components take a long time to Render, because JS is single threaded and the page is “suspended”. React rendering is no longer responsive to user actions, such as button clicks. This makes for a poor user experience.

To solve this problem, React introduces sharding, which is used in a single rendering process. Not all nodes are rendered consecutively.

For example, if a render takes 100ms, React splits the render into 10 slices, rendering one slice at a time. When one slice is executed, React checks to see if there are any high-priority things that need to be done. If there are, it cedes control, stops rendering, and responds to the event first to avoid “fake death”. After the event processing is complete, continue rendering. This causes some lifecycle functions to be called multiple times. The React Lifecycle hooks were tweaked with the introduction of React Fiber.

While it’s still experimental, React is the way of the future. In the latest React 17 release, most of the updates relate to this mode. So it’s only a matter of time before the mode is officially launched.

React Fiber & Concurrent Mode

Impact of Concurrent Mode on state management

Current Redux and MOBx are not compatible with Concurrent Mode. This is because in this mode, rendering can be triggered at any time. As shown below: when a component is rendering a part of it, rendering is stopped, execution is handed over, execution is acquired for a period of time, and rendering is picked up where it left off.

What’s the problem with that? The status is inconsistent.

How does Recoil support Concurrent Mode

In the latest version of Recoil, some experimental apis have been provided to support this pattern. Recoil will re-render the entire node tree to avoid state inconsistencies if it detects that the state has changed during rendering. This mechanism is efficient for now and will continue to improve performance.

Continuous performance improvement

Optimized memory management, more efficient state support for big data, etc.

Continuous iteration

Developing tools, support for synchronization with external data, etc.

That’s all the core features of Recoil so far. To sum up, the core points mainly include: state decentralized management, support hook Mode only, support Concurrent Mode.

I’ll briefly demonstrate the use of some of Recoil’s core apis.

Based on using

Quick start

For normal operation, all components that need to share state need to be included in the RecoilRoot component. Yes, the implementation of the source code is to use Context.

import React from 'react';
import { RecoilRoot } from 'recoil';
function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}
Copy the code

Define state

Recoil provides two functions to define a shared state, but let’s start with the most commonly used Atom.

The atom function defines a state.

import { atom } from 'recoil';
export const selectedIDs = atom({
  key: 'selectedIDs'.default: []})Copy the code

By printing the return value, we can see that it is an object of type RecoilState. At this point we’ve defined a state. This state can be subscribed to and modified by any component, and when the value of this state changes, all components subscribed to this state trigger rerendering.

Subscribe/Update status:

Recoil provides four hooks that allow us to subscribe and update our status within the component:

UseRecoilState: Returns a tuple where we can get a reference to the state and a method to update the state, the same as useState.

import { useRecoilState } from 'recoil';
import { selectedIDs } from '.. /state';
const App = props= > {
  const [sIds, setSIds] = useRecoilState(selectedIDs);
}
Copy the code

UseRecoilValue: This method can be used when we only need to subscribe to the status and no updates are required.

import { useRecoilValue } from 'recoil';
import { selectedIDs } from '.. /state';
const App = props= > {
  const sIds= useRecoilValue(selectedIDs);
}
Copy the code

UseSetRecoilState: This method can be used when we only need to update and do not need to subscribe. When the status is updated, the component is not re-rendered.

import { useSetRecoilState } from 'recoil';
import { selectedIDs } from '.. /state';
const App = props= > {
  const setSIds = useSetRecoilState(selectedIDs);
}
Copy the code

UseResetRecoilState: This method lets us reset the state to the default. When the status is updated, the component is not re-rendered.

import { useResetRecoilState } from 'recoil';
import { selectedIDs } from '.. /state';
const App = props= > {
  const resetSIds = useResetRecoilState(selectedIDs);
  // do something
  resetSIds();
}
Copy the code

Use the advanced

The derived condition

Recoil favors Minimal, or Minimal granularity, when it comes to state partitioning. This not only avoids unnecessary subscriptions and component updates, but also keeps our state from being redundant.

For example, in the above Item, the state of the selection box does not need to be defined independently, because the position of the selection box is determined by the selected Item. Through the selected state of the Item, we can calculate the position of the selection box. This is state derived data.

This is also Recoil’s second core API: Selector.

With the selector function, we can rely on an existing state (defined by Atom or selector) to define a derived state. When the dependent state is updated, the new value is automatically recalculated and the subscription component is triggered to update.

Selector, like Atom, gets a Recoil state that the component can subscribe to.

// state.js
export const selectBorderInfo = selector({
  key: 'selectBorderInfo'.get: ({get}) = > {
    const sIds = get(selectedIDs);
    const items = sIds.map(id= > get(itemFamily(id)));
    return calSelector(items)
  }
})
Copy the code
// selector.js
import { selectBorderInfo } from '.. /state';
import { useRecoilValue } from 'recoil';
export default (props) => {
  const selectBorder = useRecoilValue(selectBorderInfo);
  // render
}
Copy the code

Derived data can also be bidirectional

When the selector option only provides a get function, the result is a read-only state that can only be subscribed to within the component using the useRecoilValue. If a scenario requires a new value for the derived state, and its dependent state changes along with it, you can set the set function in the original option. When the set function is set, a writable state is obtained.

const proxySelector = selector({
  key: 'ProxySelector'.get: ({get}) = >({... get(myAtom),extraField: 'hi'}),
  set: ({set}, newValue) = > set(myAtom, newValue),
});
Copy the code

Asynchronous state

In real business development, the initial value of a state should be synchronized from the server, but HTTP requests are delayed, so we usually use the following approach: pull the data after the component is created, fetch the data and then render it again.

useEffect(() = >{(async() = > {const list = await getGameList()
    setGames(list)
  })()
}, [])
Copy the code

Earlier we introduced selector, which can define derived state. In addition, it has a powerful feature that allows you to define asynchronous states, either by returning a Promise in the GET argument to option or using async syntax. Asynchronous state can be defined, so asynchronous state support is very convenient.

export const gameList = selector<game.Game[]>({
  key: 'gameList'.get: async() = > {return await getGameList()
  }
})

const [games, setGames] = useRecoilState<game.Game[]>(gameList)
Copy the code

Since the request is asynchronous, the first time the component renders, the data has not yet been requested back. React renders an error (again, a Promise object).

There are two ways to handle this request process:

  1. Work with React native features react. Suspense, ErrorBoundary
  2. The Loadable Recoil.
return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>} ><UserInfo userID={1} />
          <UserInfo userID={2} />
          <UserInfo userID={3} />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
Copy the code
function UserInfo({userID}) {
  const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
  switch (userNameLoadable.state) {
    case 'hasValue':
      return <div>{userNameLoadable.contents}</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      throwuserNameLoadable.contents; }}Copy the code

The main thing to note is that the selector might be repeated multiple times, so the result would be cached, and it should be a pure function, with the same input arguments and dependencies, and it should get the same value.

Similarly, when using asynchronous state, the same input is required and the resulting value is the same. Only one asynchronous query will be executed for the same query parameter.

snapshot

Redux is centrally managed by the state. Generally, there is only one state and the update is reduced by reducer, so the state update can be predicted, monitored and logged.

Recoil uses decentralized state management, which makes it impossible to record and monitor the state of the entire application.

So Recoil provides Snapshot. It allows us to take a snapshot of all the states in the current application. We can persist state, observe, monitor, and manage state changes.

Usage scenarios include: share and undo redo. Using a status snapshot, you can synchronize and share the status at each point in time. The undo redo function can be easily implemented by using snapshots.

Recoil summary

Recoil is, so to speak, a wheel developed for the future. With the update iteration of React, especially the promotion of Concurrent Mode, Recoil has great potential for future development.

You probably won’t be using it in actual project development anytime soon. Small projects can also be tried.

Some of the features and ideas Recoil offers, such as state atomization, state collections, and state caching, are good and worth learning.

Tools are tools after all, there is no good or bad, only suitable, learn about new things, learn its principles and ideas, and use according to the scene, if it is just simple data sharing, do not need other fancy functions, use native functions.

Inevitably, Recoil has some downsides: the new tool, which is not yet stable, is not optimal in performance. In addition, the API provides a lot of scattered and small functions, large-scale project development may need secondary development and so on.

Still, you can expect Recoil to evolve and iterate, and embrace Hook and Concurrent modes.

React Native state sharing mode

In the Function Component of React, we can use useState to manage internal state. But if you want to share data between components, it becomes less convenient. Based on the native functionality provided by React, there are two ways to implement data sharing between components: state promotion and Context.

The state. Not only need to pass layers of props, which makes it difficult to maintain and extend, increasing the coupling of components; The intermediate components do not need to share data, but also need to receive and pass it down; Every time a child state is updated, it causes all nodes to be updated, adding a lot of unnecessary rendering. Therefore, this method is not considered when more than two layers are nested.

The Context. Context provides a way to share values between components without having to explicitly pass props layer by layer through the component tree. Context is a production-consumer model in which the components that need to share the state subscribe to all the states, and when the Context data changes, the subscribed components trigger updates and get the latest values. After version 16.8, React also provides a related hook: useContext, making it easier to useContext.

Context Usage Example

context.js

// context.js
export const state = {
  items: []}export const Context  = React.createContext();
Copy the code

app.js

// app.js
import { Context, state } from './context';

function App() {
  const [state, setState] = useState(state);
  return (
    <Context.Provider value={{ state.setState}} >
      <div style={{ display: 'flex' }}>
        <List />
        <Canvas />
        <Property />
      </div>
    </Context.Provider >
  )
}

export default App;
Copy the code

List.js

// List.js
import React, { useContext } from 'react';
import { Context, state } from './context';

function List() {
  const { state, setState } = useContext(Context);
  return (
    <div>
      {state.items.map(i => {
          return <div key={id}>{`title: ${title}  X: ${x}  Y: ${y}`}</div>             })}
    </div>
  );
}
export default List;
Copy the code

When we use Context, we usually need to push the shared data up to the top level, and the data update actually depends on the state update of the uppermost component. Components get shared data through subscriptions (useContext).

We can see that this is very similar to the key to third-party state management. In fact, all third-party libraries Redux, Mobx, and Recoil today are no exception. All source code uses Context to achieve state sharing.

Context can fully realize data sharing and unified management across components, and is relatively simple to use. However, in actual project development, we tend to choose third-party libraries such as Redux /mobx instead of using Context directly. In my opinion, there are mainly the following considerations:

  1. By the time Content launched, third-party libraries such as Redux were already popular.
  2. The Context is kind of free and uncontrollable. Each subscription component can modify the data at will.
  3. Avoid reinventing the wheel. A third-party library encapsulates React and provides simple apis for developers to improve development efficiency and code maintainability.
  4. Third-party libraries provide some extended functionality. Such as logs, snapshots, and so on.

Learn more about Context.