The introduction

The React state manager library is Redux, but Redux has some problems. It requires a lot of template code. We need to agree that the new state object is new. If we do not use the new object, we may not update it. This is a common problem of redux state not updating, so we need to ensure that the developer has to introduce libraries such as IMmer. In addition, Redux itself is a framework-independent library that needs to be combined with Redux-React to be used with React. We have to use libraries like Redux Toolkit or Rematch that have a lot of best practices built in and redesigned interfaces, but at the same time increase the learning cost of developers. React state management wheels are constantly emerging. Here is recoil, a react state management library designed for the future.

Introduction to the

Recoil’s slogan is simple: A State management library for React. It’s not a framework-independent state library; it was created specifically for React.

Like React, Recoil is facebook’s open source library. It is officially claimed to have three main features:

  1. Minimal andReactish: Minimal andReact style API.

  2. Data-flow Graph: Data Flow Graph Support for derived data and asynchronous queries are pure functions with efficient subscriptions inside.

  3. Cross-app Observation: cross-application monitoring, enabling overall state monitoring.

Basic design idea

Let’s say there’s a scenario where the corresponding state changes usJust need toUpdate the second node in the list and the second node in the canvas.Without using a third external state management library, using the Context API might look like this:

We may need many separate providers for nodes that only need to be updated, so that the and providers that actually use the child nodes of the state are actuallycouplingWhen we use the state, we need to care about whether there is a corresponding provider. If we use redux, in fact, if it is only a state update, all the subscription functions will run again. Even if we use selctor to shallow compare the two states and prevent the update of the React tree, but once the number of subscribed nodes is very large, it will actually cause performance problems.

Recoil divides the states into atoms, and the React component tree will subscribe only to the states they need. In this scenario, the items on the left and right of the component tree subscribe to different atoms, and when the atoms change, they only update the corresponding subscribed node.Recoil also supports “derived states,” which means that existing atoms can be combined into a new state, and that the new state can also become a dependency of other states.Not only synchronous selctors are supported, recoil also supports asynchronous selctors, and recoil’s only requirement for selctors is that they be pure functions.Recoil’s design idea is that we split the states into Atom by atom, and then derived more states from Selctor. Finally, the React component tree subscribes to the required states. When there are atomic state updates, only the changing atoms and their downstream nodes subscribe to their components. In other words, Recoil actually built aDirected acyclic graphThis graph is orthogonal to the React tree, and its state is exactly the same as the React treeThe decoupling.

Simple usage

So let’s take a look at the simple usage. Unlike Redux, which is a framework-independent state management library, Recoil’s API is “React style” since it was designed specifically for React. Recoil only supports the hooks API, which is pretty simple to use. Here’s a Demo:

import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue
} from "recoil";

export default function App() {
  return (
    <RecoilRoot>
      <Demo />
    </RecoilRoot>
  );
}

const textState = atom({
  key: "textState".default: "" 
});

const charCountState = selector({
  key:'charCountState'.get: ({get}) = > {
    // A pure function is required
    const text = get(textState)
    return text.length
  }
})

function Demo() {
  const [text, setText] = useRecoilState(textState);
  const count = useRecoilValue(charCountState)
  return (
    <>
      <input value={text} onChange={(e)= > setText(e.target.value)} />
      <br />
      Echo: {text}
      <br />
      charCount: {count}
    </>
  );
}
Copy the code
  • Like React Redux, Recoil also has a Providar — RecoilRoot, which is used to share some methods and states globally.

  • Atom is recoil’s smallest unit of state. Atom means that a value can be read, written, and subscribed to, and it must have a key that is unique and immutable from other Atoms. Atom allows you to define a piece of data.

  • A Selector is a bit like a Selector in react-redux, which is used to “derive” the state, except that it differs from a Selector in react-Redux:

    • The selector for react-Redux is a pure function that always runs when globally unique states change, calculating new states from globally unique states.

    • In Recoil, the get of the selector options. is also required to be a pure function in which the get method is passed in to get the other Atom. It actually recalculates if and only if the dependent Atom changes and there’s a component that subscribs to the selector, which means that the evaluated value is cached, and if the dependency hasn’t changed, it’s actually just read from the cache and returned. And the selector also returns an Atom, which means that the derived state is actually an atom, and can actually be relied on by other selectors.

It is obvious that Recoil collects dependencies through the get input parameter in the GET function. Recoil supports dynamic collection of dependencies, which means that GET can be called in conditions:

const toggleState = atom({key: 'Toggle'.default: false});

const mySelector = selector({
  key: 'MySelector'.get: ({get}) = > {
    const toggle = get(toggleState);
    if (toggle) {
      return get(selectorA);
    } else {
      returnget(selectorB); }}});Copy the code

asynchronous

Recoil supports asynchrony naturally, and it’s easy to use. You don’t need to configure any asynchrony plug-ins.

const asyncDataState = selector({
  key: "asyncData".get: async ({get}) => {
   // A pure function is required
    return awaitgetAsyncData(); }});function AsyncComp() {
  const asyncData = useRecoilValue(asyncDataState);
  return <>{asyncData}</>;
}
function Demo() {
  return (
    <React.Suspense fallback={<>loading...</>} ><AsyncComp />
    </React.Suspense>
  );
}
Copy the code

Recoil naturally supports React suspense, so when using useRecoilValue to fetch data, if the asynchronous state is pending, the promise will be raised by default. React will show the contents of the fallback; If an error is reported, the contents are also thrown and captured by the outer ErrorBoundary. If you don’t want to use this feature, you can use useRecoilValueLoadable to get the asynchronous status directly, demo:

function AsyncComp() {
  const asyncState = useRecoilValueLoadable(asyncDataState);
  if (asyncState.state === "loading") {
    return <>loading...</>;
  }
  if (asyncState.state === "hasError") {
    return <>has error....</>;
  }
  if (asyncState.state === "hasValue") {
    return <>{asyncState.contents}</>;
  }
  return null;
}
Copy the code

Note also that asynchronous results are cached by default, in fact all unchanged upstream selctor results are cached. That is, if the asynchronous dependency has not changed, the asynchronous function is not re-executed and the cached value is returned. That’s why it’s so important to emphasize that the selector configuration item get is a pure function.

Dependency on external variables

If the state is actually dependent on external variables, recoil has selectorFamily support:

const getUserInfoState = selectorFamily({
  key: "userInfo".get: (userId) = > ({ get }) = > {
    return queryUserState({userId: id, xxx: get(xxx) }); }});function MyComponent({ userID }) {
  
  const number = useRecoilValue(getUserInfoState(userID));
  / /...
}
Copy the code

The external parameter and key will generate a globally unique key that identifies the state, meaning that if the external variable has not changed or the dependency has not changed, the state will not be recalculated and the cached value will be returned.

The source code parsing

If you look at this and just implement those simple examples, you might say, “That’s it”? Recoil’s react source code is a legacy of the react source code, but it’s very difficult to read.

Its source code core functions are divided into several parts:

  • Graph logic

  • Nodeatom and Selctor are internally abstracted as nodes

  • RecoilRoot is basically a set of external RecoilRoot,

  • RecoilValue Specifies the type of external exposure. That is, the return value of Atom, selctor.

  • Hooks Related to the use of hooks.

  • Snapshot Indicates the status Snapshot. It provides status records and rollback.

  • Some other unreadable code…

Here is to talk about their own these days to see the shallow understanding of the source code, welcome the bosses to correct.

Concurrent mode support

In case you get confused, let’s talk about my biggest concern, recoil’s support for Conccurent’s idea, which may not be quite right (there are no resources available online, so you’re welcome to discuss them).

Cocurrent mode

React Cocurrent Mode is a set of new features that help RACT applications stay responsive and elegant with user device capabilities and network speeds. React moves to Fiber architecture for concurrent mode implementation. React has two phases under the new architecture:

  • Rendering stage

  • Commit phase

During the render phase, React can render the component tree based on task priority, so the current render task may be interrupted due to insufficient priority or no time remaining in the current frame. Subsequent dispatchers re-perform the current task rendering.

Inconsistency between UI and state

Since React now gives up control flow, anything can happen between the start and end of rendering, and some hooks are cancelled for this reason. For a third-party state library, for example, if an asynchronous request changes the external state during this time, React will continue rendering where it was interrupted last time and will read the new state value. Will happen,State and UI Don’t agreeIn the case.

Recoil’s solution

Global data structure

atom

Atom actually calls baseAtom, which has a closure variable defaultLoadable inside it that records the current default value. The getAtom and setAtom functions are declared and passed to the registerNode to complete the registration.

function baseAtom(options){
   / / the default value
   let defaultLoadable = isPromise(options.default) ? xxxx : options.default
   
   function getAtom(store,state){
       if(state.atomValues.has(key)){
           // If the current state has the value of this key, return it directly.
           return state.atomValues.get(key)
       }else if(state.novalidtedAtoms.has(key)){
          / /.. Some logic
       }else{
           returndefaultLoadable; }}function setAtom(store, state, newValue){
      if (state.atomValues.has(key)) {
          const existing = nullthrows(state.atomValues.get(key));
          if (existing.state === 'hasValue' && newValue === existing.contents) {
              // Return an empty map if it is equal
            return new Map();
          }
        }
      / /...
      // Return key --> Map of new loadableValue
      return new Map().set(key, loadableWithValue(newValue));      
   }
   
   function invalidateAtom(){
       / /...
   }
   
   
   
  const node = registerNode(
    ({
      key,
      nodeType: 'atom'.get: getAtom,
      set: setAtom,
      init: initAtom,
      invalidate: invalidateAtom,
      // Ignore other configurations...}));return node; 
}

function registerNode(){
  if (nodes.has(node.key)) {
    / /...
  }
  nodes.set(node.key, node);

  const recoilValue =
    node.set == null
      ? new RecoilValueClasses.RecoilValueReadOnly(node.key)
      : new RecoilValueClasses.RecoilState(node.key);

  recoilValues.set(node.key, recoilValue);
  return recoilValue;
}
Copy the code

selector

Since a selector can also be passed a set configuration item, we won’t analyze it here.

function selector(options){
    const {key, get} = options
    const deps = new Set(a);function selectorGet(){
       // Check for loop dependencies
       return detectCircularDependencies(() = >
          getSelectorValAndUpdatedDeps(store, state),
      );
    }
    
    function getSelectorValAndUpdatedDeps(){
        const cachedVal = getValFromCacheAndUpdatedDownstreamDeps(store, state);
        if(cachedVal ! =null) {
          setExecutionInfo(cachedVal, store);
          // If there is a cached value, return it directly
          return cachedVal;
        }
        / / parse the getter
         const [loadable, newDepValues] = evaluateSelectorGetter(
          store,
          state,
          newExecutionId,
        );
        // Cache the results
        maybeSetCacheWithLoadable(
          state,
          depValuesToDepRoute(newDepValues),
          loadable,
        );
        / /...
        return lodable
    }
   
    function evaluateSelectorGetter(){
        function getRecoilValue(recoilValue){
               const { key: depKey } = recoilValue
               dpes.add(key);
               / / in the graph
               setDepsInStore(store, state, deps, executionId);
               const depLoadable = getCachedNodeLoadable(store, state, depKey);
               if (depLoadable.state === 'hasValue') {
                    return depLoadable.contents;
              }
                throw depLoadable.contents;
        }
        const result = get({get: getRecoilValue});
        const lodable = getLodable(result);
        / /...

        return [loadable, depValues];
    }

    return registerNode<T>({
          key,
          nodeType: 'selector'.peek: selectorPeek,
          get: selectorGet,
          init: selectorInit,
          invalidate: invalidateSelector,
          / /...}); }}Copy the code

hooks

useRecoilValue && useRecoilValueLoadable

  • The useRecoilValue base actually relies on useRecoilValueLoadable, and if the return value of useRecoilValueLoadable is a promise, then it will be thrown out.

  • UseRecoilValueLoadable will first subscribe to RecoilValue changes in useEffect and then call ForceUpdate to re-render if the changes are found to be different. The value is returned in lodable form by calling node’s get method.

function useRecoilValue<T> (recoilValue: RecoilValue<T>) :T {
  const storeRef = useStoreRef();
  const loadable = useRecoilValueLoadable(recoilValue);
  // If it is a promise, throw it.
  return handleLoadable(loadable, recoilValue, storeRef);
}

function useRecoilValueLoadable_LEGACY(recoilValue){
    const storeRef = useStoreRef();
    const [_, forceUpdate] = useState([]);
    
    const componentName = useComponentName();
    
    useEffect(() = > {
        const store = storeRef.current;
        const storeState = store.getState();
        / / is actually in storeState nodeToComponentSubscriptions built node - > subscribe to mapping function
        const subscription = subscribeToRecoilValue(
          store,
          recoilValue,
          _state= > {
            // Enable some features in code through GKX to facilitate unit testing and code iteration.
            if(! gkx('recoil_suppress_rerender_in_callback')) {
              return forceUpdate([]);
            }
            const newLoadable = getRecoilValueAsLoadable(
              store,
              recoilValue,
              store.getState().currentTree,
            );
            // A small optimization
            if(! prevLoadableRef.current? .is(newLoadable)) { forceUpdate(newLoadable); } prevLoadableRef.current = newLoadable; }, componentName, );/ /...
        // release
         return subscription.release;    
    })
    
    // Essentially calling the Node.get method. And then do something else
    const loadable = getRecoilValueAsLoadable(storeRef.current, recoilValue);

    const prevLoadableRef = useRef(loadable);
    useEffect(() = > {
        prevLoadableRef.current = loadable;
    });
    return loadable;
}
Copy the code

One interesting point here is that the implementation of useComponentName is a bit of a hack: since we usually agree on hooks that start with use, you can use the call stack to find the first function that is called that starts with either the use function name or the component name. Of course production environments are not available due to code obfuscation.

function useComponentName() :string {
  const nameRef = useRef();
  if (__DEV__) {
      if (nameRef.current === undefined) {
        const frames = stackTraceParser(new Error().stack);
        for (const {methodName} of frames) {
          if(! methodName.match(/\buse[^\b]+$/)) {
            return (nameRef.current = methodName);
          }
        }
        nameRef.current = null;
      }
      return nameRef.current ?? '<unable to determine component name>';
  }
  return '<component name not available>'; 
}
Copy the code

UseRecoilValueLoadable_MUTABLESOURCE is basically the same, except that in the subscription function we went from calling foceUpdate manually to calling the parameter callback.

function useRecoilValueLoadable_MUTABLESOURCE(){
    / /...
    
    const getLoadable = useCallback(() = > {
        const store = storeRef.current;
        const storeState = store.getState();
        / /...
        constTreeState = storeState. CurrentTree;return getRecoilValueAsLoadable(store, recoilValue, treeState);
    }, [storeRef, recoilValue]);
  
    const subscribe = useCallback(
    (_storeState, callback) = > {
      const store = storeRef.current;
      const subscription = subscribeToRecoilValue(
        store,
        recoilValue,
        () = > {
          if(! gkx('recoil_suppress_rerender_in_callback')) {
            return callback();
          }
          const newLoadable = getLoadable();
          if(! prevLoadableRef.current.is(newLoadable)) { callback(); } prevLoadableRef.current = newLoadable; }, componentName, );return subscription.release;
    },
    [storeRef, recoilValue, componentName, getLoadable],
  );
    const source = useRecoilMutableSource();
    const loadable = useMutableSource(source, getLoadableWithTesting, subscribe);
    const prevLoadableRef = useRef(loadable);
    useEffect(() = > {
        prevLoadableRef.current = loadable;
    });
    return loadable;
}
Copy the code

useSetRecoilState & setRecoilValue

UseSetRecoilState eventually is called queueOrPerformStateUpdate, updated in the queue waiting to call

function useSetRecoilState(recoilState){
  const storeRef = useStoreRef();
  return useCallback(
    (newValueOrUpdater) = > {
      setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
    },
    [storeRef, recoilState],
  );
}

function setRecoilValue<T> (store, recoilValue, valueOrUpdater,) {
  queueOrPerformStateUpdate(store, {
    type: 'set',
    recoilValue,
    valueOrUpdater,
  });
}
Copy the code

QueueOrPerformStateUpdate, after the operation is complicated here is simplified to three steps, as follows;

function queueOrPerformStateUpdate(){
    / /...
    //atomValues sets the value
    state.atomValues.set(key, loadable);
    // Add keys to dirtyAtoms.
    state.dirtyAtoms.add(key);
    // Get it through storeRef.
    notifyBatcherOfChange.current()
}

Copy the code

Batcher

Recoil implemented a batch update mechanism in-house.

function Batcher({
  setNotifyBatcherOfChange,
}: {
  setNotifyBatcherOfChange: (() => void) = >void,}) {
  const storeRef = useStoreRef();

  const [_, setState] = useState([]);
  setNotifyBatcherOfChange(() = > setState({}));

  useEffect(() = > {
      endBatch(storeRef);
  });

  return null;
}


function endBatch(storeRef) {
    const storeState = storeRef.current.getState();    
    const {nextTree} = storeState;
    if (nextTree === null) {
      return;
    }
    / / tree
    storeState.previousTree = storeState.currentTree;
    storeState.currentTree = nextTree;
    storeState.nextTree = null;
    
    sendEndOfBatchNotifications(storeRef.current);
}

function sendEndOfBatchNotifications(store: Store) {
  const storeState = store.getState();
  const treeState = storeState.currentTree;
  const dirtyAtoms = treeState.dirtyAtoms;
 // Get all downstream nodes.
 const dependentNodes = getDownstreamNodes(
    store,
    treeState,
    treeState.dirtyAtoms,
  );
  for (const key of dependentNodes) {
      const comps = storeState.nodeToComponentSubscriptions.get(key);

      if (comps) {
        for (const [_subID, [_debugName, callback]] ofcomps) { callback(treeState); }}}}/ /...
}

Copy the code

conclusion

React has a lot of state management libraries, but recoil’s ideas are still very advanced, and the community is also very concerned about the new wheel. Currently githubstar14k. Because Recoil is not yet a stable version, NPM is not widely downloaded and is not recommended for use in production. However, with the release of React18, recoil will be updated to a stable version, which will be used more and more, so you can give it a try.

Reference documentation

Recoil

Close reading of Recoil

Recoil usage and principle analysis

React state management comparison and principle implementation