Talk about Concent & Recoil in detail, explore the new development model of react data flow
Published: 2020-11-20 04:04:03

Open source is not easy, thank you for your support, [❤ star me if you like concent ^_^](https://github.com/concentjs/concent)

## Preface
Recently, more and more of the latest state management solutions from facebook [recoil](https://github.com/facebookexperimental/Recoil) have been mentioned gradually. Although it is still in an experimental state, everyone has already started privately. I want to try it. After all, I was born famous and has fb endorsement, and I will definitely shine.

However, after I experienced recoil, I remained skeptical about the **precise update** advertised in it, and there were some misleading suspicions. This point will be analyzed separately below. Whether it is misleading, readers can naturally draw conclusions after reading this article , In short, this article mainly analyzes the code style differences between `Concent` and `Recoil`, and discusses their new influence on our future development model, and what kind of change in thinking needs to be done.

## 3 major genres of data flow solutions

The current mainstream data flow solutions can be divided into the following three categories according to the form

-redux genre
Redux, other works derived from redux, and works similar to redux ideas, representative works include dva, rematch, and so on.

-mobx genre
With the help of definePerperty and Proxy to complete data hijacking, so as to achieve the representative of the purpose of responsive programming, there are also many mobx-like works, such as dob.

-Context genre
Context here refers to the Context api that comes with React. Data flow solutions based on Context api are usually lightweight, easy to use, and less overview. Representative works are unstated, constate, etc. The core code of most works may not exceed 500 Row.

At this point, let's see which category `Recoil` should belong to? Obviously it belongs to the Context genre according to its characteristics, so the main light weight we said above
`Recoil` is not applicable anymore. Open the source code library and find that the code is not done in a few hundred lines. Therefore, it is not necessarily lightweight based on the `Context api` that is easy to use and powerful. It can be seen that `facebook` is against `Recoil` It is ambitious and gives high hopes.

Let's also see which category `Concent` belongs to? After the `v2` version of `Concent`, the data tracking mechanism was refactored, and the defineProperty and Proxy features were enabled, so that the react application not only retains the pursuit of immutability, but also enjoys the performance improvement benefits of runtime dependency collection and precise UI update. Now that defineProperty and Proxy are enabled, it seems that `Concent` should belong to the mobx genre?

In fact, `Concent` belongs to a brand new genre. It does not rely on React's Context API, does not destroy the form of the React component itself, maintains the philosophy of pursuing immutability, and only establishes a logical layer state on top of React's own rendering scheduling mechanism. Distributed scheduling mechanism, defineProperty and Proxy are only used to assist in collecting instances and derived data dependent on module data, and modifying the data entry is still setState (or dispatch, invoke, sync based on setState encapsulation), so that `Concent` can be accessed with zero intrusion Into the react application, true plug-and-play and non-perceived access.

The core principle of **plug and play** is that `Concent` builds a global context parallel to the react runtime, carefully maintains the attribution relationship between the module and the instance, and at the same time takes over the update entry setState of the component instance , Keep the original setState as reactSetState. When the user calls setState, in addition to calling reactSetState to update the current instance ui, Concent also intelligently judges whether there are other instances in the submitted state that care about its changes, and then take them out and execute these instances in turn The reactSetState achieves the purpose of all the states are synchronized.

![](https://user-gold-cdn.xitu.io/2020/6/9/17296cb051760351?w=1055&h=458&f=png&s=60206)

## Recoil first experience
Let's take the commonly used counter as an example to get familiar with the four frequently used APIs exposed by `Recoil`
-atom, defines the state
-selector, define derived data
-useRecoilState, consumption state
-useRecoilValue, consumption derived data

### Define status
Use the `atom` interface externally, define a state where the key is `num` and the initial value is `0`

```js
const numState = atom({
  key: "num",
  default: 0
});
```

### Define derived data
Use the `selector` interface externally, define a key as `numx10`, and the initial value is calculated again by relying on `numState`

```js
const numx10Val = selector({
  key: "numx10",
  get: ({ get }) => {
    const num = get(numState);
    return num * 10;
  }
});
```

### Define asynchronous derived data

The `get` of `selector` supports defining asynchronous functions
> The point to note is that if there is a dependency, the dependency must be written before the asynchronous logic is executed.

```js
const delay = () => new Promise(r => setTimeout(r, 1000));

const asyncNumx10Val = selector({
  key: "asyncNumx10",
  get: async ({ get }) => {
    // !!! This sentence cannot be placed under delay, the selector needs to be synchronized to determine the dependency
    const num = get(numState);
    await delay();
    return num * 10;
  }
});
```


### Consumption status
Use the `useRecoilState` interface in the component to pass in the state you want to get (created by `atom`)

```jsx
const NumView = () => {
  const [num, setNum] = useRecoilState(numState);

  const add = ()=>setNum(num+1);

  return (
    <div>
      {num}<br/>
      <button onClick={add}>add</button>
    </div>
  );
}
```

### Consumption derived data
Use the `useRecoilValue` interface in the component to pass in the derived data you want to get (created by `selector`). Both synchronous and asynchronous derived data can be obtained through this interface

```jsx
const NumValView = () => {
  const numx10 = useRecoilValue(numx10Val);
  const asyncNumx10 = useRecoilValue(asyncNumx10Val);

  return (
    <div>
      numx10 :{numx10}<br/>
    </div>
  );
};
```

### Render them to see the results

Expose these two defined components, [View online example](https://codesandbox.io/s/recoiljs-is-meant-to-rock-your-react-world-oljjf?file=/src/List. jsx:636-730)
```jsx
export default ()=>{
  return (
    <>
      <NumView />
      <NumValView />
    </>
  );
};
```

The top node wraps `React.Suspense` and `RecoilRoot`, the former is used to meet the needs of asynchronous calculation functions, the latter is used to inject the context of `Recoil`

```jsx
const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <React.Suspense fallback={<div>Loading...</div>}>
      <RecoilRoot>
        <Demo />
      </RecoilRoot>
    </React.Suspense>
  </React.StrictMode>,
  rootElement
);
```


![](https://user-gold-cdn.xitu.io/2020/6/13/172ad515a0de09ba?w=480&h=270&f=gif&s=1668519)

## Concent first experience
If you have read the concent document (still under construction...), some people may think that there are too many APIs and it is difficult to remember. In fact, most of them are optional syntactic sugar. Let’s take counter as an example, and only need to use The following two apis
-run, define module status (required), module calculation (optional), module observation (optional)
> After running the run interface, a concent global context will be generated
-setState, modify state

### Define status & modify status

In the following example, we first break away from ui and directly complete the purpose of defining state & modifying state

```js
import {run, setState, getState} from "concent";

run({
  counter: {// Declare a counter module
    state: {num: 1 }, // define state
  }
});

console.log(getState('counter').num);// log: 1
setState('counter', {num:10});// Modify the num value of the counter module to 10
console.log(getState('counter').num);// log: 10
```

We can see that this is very similar to `redux`, a single state tree needs to be defined, and the first-level key guides users to modularize the management of data.

### Introducing reducer
In the above example, we directly drop one `setState` to modify the data, but the real situation is that there are many synchronous or asynchronous business logic operations before the data falls, so we fill in the `reducer` definition for the module to declare the modification of the data Method collection.

```js
import {run, dispatch, getState} from "concent";

const delay = () => new Promise(r => setTimeout(r, 1000));

const state = () => (( num: 1 ));//State statement
const reducer = {// reducer declaration
  inc(payload, moduleState) {
    return {num: moduleState.num + 1 };
  },
  async asyncInc(payload, moduleState) {
    await delay();
    return {num: moduleState.num + 1 };
  }
};

run({
  counter: {state, reducer}
});
```

Then we use `dispatch` to trigger the method of modifying the state
> Because dispatch returns a Promise, we need to wrap it up with an async to execute the code

```js
import {dispatch} from "concent";

(async ()=>{
  console.log(getState("counter").num);// log 1
  await dispatch("counter/inc");// Synchronous modification
  console.log(getState("counter").num);// log 2
  await dispatch("counter/asyncInc");// Asynchronous modification
  console.log(getState("counter").num);// log 3
})()
```

Note that the dispatch call is based on the string matching method. The reason why this call method is retained is to take care of the scenes that need to be dynamically called.

```js
import {dispatch} from "concent";

await dispatch("counter/inc");
// change into
await dispatch(reducer.inc);
```

In fact, the `reducer` set defined by the `run` interface has been centrally managed by `concent` and allows users to call it in the way of `reducer.${moduleName}.${methodName}`, so here we can even base it on the `reducer` Make a call

```js
import {reducer as ccReducer} from'concent';

await dispatch(reducer.inc);
// change into
await ccReducer.counter.inc();
```

### Connect to react

The above example mainly demonstrates how to define the state and modify the state, then we need to use the following two APIs to help the react component generate the instance context (equivalent to the rendering context mentioned in the vue 3 setup), and obtain the consumption concentration module Data capabilities

-register, the registered component is a concent component
-useConcent, register the function component as a concent component

```jsx
import {register, useConcent} from "concent";

@register("counter")
class ClsComp extends React.Component {
  changeNum = () => this.setState({ num: 10 })
  render() {
    return (
      <div>
        <h1>class comp: {this.state.num}</h1>
        <button onClick={this.changeNum}>changeNum</button>
      </div>
    );
  }
}

function FnComp() {
  const {state, setState} = useConcent("counter");
  const changeNum = () => setState({ num: 20 });
  
  return (
    <div>
      <h1>fn comp: {state.num}</h1>
      <button onClick={changeNum}>changeNum</button>
    </div>
  );
}
```

Note that the difference between the two writing methods is very small, except for the different definition of components, in fact, the rendering logic and data sources are exactly the same.

### Render them to see the results

[Online example](https://codesandbox.io/s/rvc-demo2-vg3uh?file=/src/index.js)
```jsx
const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <div>
      <ClsComp />
      <FnComp />
    </div>
  </React.StrictMode>,
  rootElement
);
```


![](https://user-gold-cdn.xitu.io/2020/6/13/172ad36db732ed63?w=480&h=271&f=gif&s=3456886)

Comparing with `Recoil`, we found that there is no top layer and no `Provider` or `Root` similar component package, the react component has been connected to the concentration, which achieves true plug-and-play and non-perceptual access, while the `api` remains It is consistent with `react`.

### Component calls reducer

Concent generates an instance context for each component instance, which is convenient for users to directly call the reducer method through `ctx.mr`
> mr is shorthand for moduleReducer, and it is legal to write it directly as ctx.moduleReducer

```js
// --------- For class components -----------
changeNum = () => this.setState({ num: 10 })
// ===> amended to
changeNum = () => this.ctx.mr.inc(10);// or this.ctx.mr.asynInc(10)

//Of course, this can also be written as ctx.dispatch call, but it is more recommended to use the above moduleReducer to call directly
//this.ctx.dispatch('inc', 10); // or this.ctx.dispatch('asynInc', 10)

// --------- For function components -----------
const {state, mr} = useConcent("counter");// useConcent returns ctx
const changeNum = () => mr.inc(20); // or ctx.mr.asynInc(10)

//For function groups, the dispatch method will also be supported
//ctx.dispatch('inc', 10); // or ctx.dispatch('asynInc', 10)
```

### Asynchronous calculation function

The `run` interface supports the extension of the `computed` attribute, which allows users to define a collection of calculation functions for derived data. They can be synchronous or asynchronous, and support one function to use the output of another function as input. In the second calculation, the input dependency of the calculation is automatically collected.

```js
 const computed = {// Define the collection of calculation functions
  numx10({ num }) {
    return num * 10;
  },
  // n:newState, o:oldState, f:fnCtx
  // Structure num, indicating that the current calculation dependency is num, and this function is triggered to recalculate only when num changes
  async numx10_2({ num }, o, f) {
    // It is necessary to call setInitialVal to give numx10_2 an initial value,
    // This function is only executed once when the first computed trigger is triggered
    f.setInitialVal(num * 55);
    await delay();
    return num * 100;
  },
  async numx10_3({ num }, o, f) {
    f.setInitialVal(num * 1);
    await delay();
    // use numx10_2 to calculate again
    const ret = num * f.cuVal.numx10_2;
    if (ret% 40000 === 0) throw new Error("-->mock error");
    return ret;
  }
}

// Configure to the counter module
run({
  counter: {state, reducer, computed}
});
```

In the above calculation function, we deliberately let `numx10_3` report an error at some point. For this error, we can define `errorHandler` in the second `options` configuration of the `run` interface to catch it.

```js
run({/**storeConfig*/}, {
    errorHandler: (err)=>{
        alert(err.message);
    }
})
```

Of course, it is better to use the `concent-plugin-async-computed-status` plugin to complete the unified management of the execution status of all module calculation functions.

```jsx
import cuStatusPlugin from "concent-plugin-async-computed-status";

run(
  {/**storeConfig*/},
  {
    errorHandler: err => {
      console.error('errorHandler', err);
      // alert(err.message);
    },
    plugins: [cuStatusPlugin], // Configure asynchronous calculation function execution status management plugin
  }
);
```

The plug-in will automatically configure a `cuStatus` module to concent, so that components can connect to it and consume the execution status data of related calculation functions

```jsx
function Test() {
  const {moduleComputed, connectedState, setState, state, ccUniqueKey} = useConcent({
    module: "counter",// belongs to the counter module, the state is obtained directly from the state
    connect: ["cuStatus"],// Connect to the cuStatus module, the status is obtained from connectedState.{$moduleName}
  });
  const changeNum = () => setState({ num: state.num + 1 });
  
  // Get the execution status of the calculation function of the counter module
  const counterCuStatus = connectedState.cuStatus.counter;
  // Of course, the execution status of the specified settlement function can be obtained in a more granular manner
  // const {['counter/numx10_2']:num1Status, ['counter/numx10_3']: num2Status} = connectedState.cuStatus;

  return (
    <div>
      {state.num}
      <br />
      {counterCuStatus.done? moduleComputed.numx10:'computing'}
      {/** Errors obtained here can be used for rendering, of course also thrown out */}
      {/** Let components like ErrorBoundary capture and render the degraded page */}
      {counterCuStatus.err? counterCuStatus.err.message:''}
      <br />
      {moduleComputed.numx10_2}
      <br />
      {moduleComputed.numx10_3}
      <br />
      <button onClick={changeNum}>changeNum</button>
    </div>
  );
}
```

![](https://user-gold-cdn.xitu.io/2020/6/13/172ad5ac79a3e9da?w=480&h=259&f=gif&s=593937)

[View online example](https://codesandbox.io/s/async-computed-35byz?file=/src/App.js:1378-2042)

## Accurate update

At the beginning, I said that I remain skeptical about the **precise update** mentioned by `Recoli`, and there are some misleading suspicions. Here we will uncover the suspicion

Everyone knows that `hook` rules cannot be written in conditional control statements, which means that the following statements are not allowed

```js
const NumView = () => {
  const [show, setShow] = useState(true);
  if(show){// error
    const [num, setNum] = useRecoilState(numState);
  }
}
```

So if the user does not use this data for a certain state in the UI rendering, changing the value of `num` somewhere will still trigger the re-rendering of `NumView`, but the `state` and `from the instance context of `concent` moduleComputed` is a `Proxy` object, which collects the dependencies required for each round of rendering in real time. This is true on-demand rendering and accurate update.

```jsx
const NumView = () => {
  const [show, setShow] = useState(true);
  const {state} = useConcent('counter');
  // When show is true, the rendering of the current instance depends on the rendering of state.num
  return {show? <h1>{state.num}</h1>:'nothing'}
}
```

![](https://user-gold-cdn.xitu.io/2020/6/13/172ad7ce248ad1e7?w=480&h=230&f=gif&s=1558232)

[Click me to view the code example](https://codesandbox.io/s/dep-collection-uiqzn?file=/src/App.js)

Of course, if the user needs to do other things when the num value has been rendered after the ui is changed, similar to the effect of `useEffect`, concent also supports users to extract it into `setup` and define `effect` to complete this Scenario requirements, compared to `useEffect`, the `ctx.effect` in the setup only needs to be defined once, and only the key name is passed. Concent will automatically compare the value of the previous moment and the current moment to determine whether to trigger the side effect function.

```jsx
conset setup = (ctx)=>{
  ctx.effect(()=>{
    console.log('do something when num changed');
    return ()=>console.log('clear up');
  }, ['num'])
}

function Test1(){
  useConcent({module:'cunter', setup});
  return <h1>for setup<h1/>
}
```

[More about effect and useEffect, please check this article](https://juejin.im/post/6844904015747104775)

## current mode

Regarding the question of whether concent supports `current mode`, let’s talk about the answer first. `concent` is 100% fully supported, or further, all state management tools are ultimately triggered by `setState` or `forceUpdate`. We As long as you don't write code with any side effects during the rendering process, letting the same state input to the power of the rendering result is safe to run in `current mode`.

`current mode` just puts forward more stringent requirements on our code.

```jsx
// bad
function Test(){
   track.upload('renderTrigger');// Report rendering trigger event
   return <h1>bad case</h1>
}

// good
function Test(){
   useEffect(()=>{
      // Even if setState is executed only once, the component may be rendered repeatedly in current mode,
      // But React internally guarantees that the side effect will only be triggered once
      track.upload('renderTrigger');
   })
   return <h1>bad case</h1>
}

```

We first need to understand the principle of current mode because the fiber architecture simulates the entire rendering stack (that is, the information stored on the fiber node), which allows React to schedule the rendering process of the component in the unit of **component**. Stop and enter the rendering again, arrange the high priority to render first, and the heavily rendered components will **slice** repeatedly rendering for multiple time periods, and the context of the concentration itself is independent of the existence of react (the access to the concentration does not require anymore The top-level package is any Provider), which is only responsible for processing the business to generate new data, and then dispatching it to the corresponding instance on demand (the state of the instance itself is an island, and concent is only responsible for synchronizing the data of the dependent store), and then react In its own scheduling process, the function that modifies the state will not be executed multiple times due to repeated reentry of the component (this requires us to follow the principle of writing code that contains side effects during the rendering process), react is only scheduling the rendering timing of the component , And the component's **interruption** and **reentry** are also aimed at this rendering process.

So the same, for concent

```jsx
const setup = (ctx)=>{
  ctx.effect(()=>{
     // effect is an encapsulation of useEffect,
     // This side effect is only triggered once in current mode (guaranteed by react)
      track.upload('renderTrigger');
  });
}

// good
function Test2(){
   useConcent({setup})
   return <h1>good case</h1>
}

```

Similarly, in the `current mode` mode of dependency collection, repeated rendering only triggers multiple collections. As long as the state input is the same, the rendering result is idempotent, and the collected dependency results are also idempotent.

```jsx
// Assuming that this is a time-consuming component to render, rendering may be interrupted in current mode
function HeavyComp(){
  const {state} = useConcent({module:'counter'});// belongs to the counter module

 // Two values ​​of num and numBig are read here, and the dependencies are collected
 // That is, only when the num and numBig of the counter module change, the re-rendering is triggered (the setState is finally called)
 // When other values ​​of the counter module change, the setState of the instance will not be triggered
  return (
    <div>num: {state.num} numBig: {state.numBig}</div>
  );
}
```

Finally, we can sort out the fact that `hook` itself is a custom hook (function without ui return) that supports the stripping of logic, and other state management is just another layer of work to guide users to strip the logic to them Under the rules, the business processing data is finally returned to the `react` component to call its `setState` or `forceUpdate` to trigger re-rendering. The introduction of `current mode` will not affect the existing state management or the new state management The solution has any impact, but it puts higher requirements on the user's ui code, so as not to cause bugs that are difficult to eliminate because of `current mode`

> For this reason, React also provides the `React.Strict` component to deliberately trigger the dual call mechanism, https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects to guide users to write more The react code conforms to the specification to adapt to the current mode provided in the future.

All new features of react are actually activated by `fiber`. With `fiber` architecture, it has derived `hook`, `time slicing`, `suspense` and the future `Concurrent Mode`, both class components and function components You can work safely in `Concurrent Mode`, as long as you follow the specifications.

Taken from: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

-Class component constructor, render, and shouldComponentUpdate methods
-Class component static getDerivedStateFromProps method
-Function component bodies
-State updater functions (the first argument to setState)
-Functions passed to useState, useMemo, or useReducer

So, `React.Strict` actually provides auxiliary APIs to guide users to write code that can run in `Concurrent Mode`. First let users get used to these restrictions, step by step, and finally launch `Concurrent Mode` .

## Conclusion

`Recoil` advocates more fine-grained control of state and derived data. The demo looks simple in writing, but in fact, it is still very cumbersome after the code is large.

```js
// define state
const numState = atom({key:'num', default:0});
const numBigState = atom({key:'numBig', default:100});
// Define derived data
const numx2Val = selector({
  key: "numx2",
  get: ({ get }) => get(numState) * 2,
});
const numBigx2Val = selector({
  key: "numBigx2",
  get: ({ get }) => get(numBigState) * 2,
});
const numSumBigVal = selector({
  key: "numSumBig",
  get: ({ get }) => get(numState) + get(numBigState),
});

// ---> Consumption status or derived data at ui
const [num] = useRecoilState(numState);
const [numBig] = useRecoilState(numBigState);
const numx2 = useRecoilValue(numx2Val);
const numBigx2 = useRecoilValue(numBigx2Val);
const numSumBig = useRecoilValue(numSumBigVal);
```

`Concent` follows the essence of `redux` single state tree, respects modular management data and derived data, and at the same time relies on the ability of `Proxy` to complete the perfect integration of **runtime dependency collection** and **immutability pursuit**.

```js
run({
  counter: {// Declare a counter module
    state: {num: 1, numBig: 100 }, // define state
    computed:{// Define the calculation, and determine the dependency when deconstructing the specific state in the parameter list
       numx2: ((num))=> num * 2,
       numBigx2: ((numBig))=> numBig * 2,
       numSumBig: ({num, numBig})=> num + numBig,
     }
  },
});

// ---> The consumption state or derivative data at ui, the dependency is generated only after the structure at ui
const {state, moduleComputed, setState} = useConcent('counter')
const {numx2, numBigx2, numSumBig} = moduleComputed;
const {num, numBig} = state;
```

So you will get:
-Dependency collection at runtime, while also following the principle of react immutability
-Everything is a function (state, reducer, computed, watch, event...), can get more friendly ts support
-Support middleware and plug-in mechanism, easy to be compatible with redux ecology
-Supports centralized and fractal module configuration, synchronous and asynchronous module loading at the same time, which is more friendly to the flexible reconstruction process of large projects


[❤ star me if you like concent ^_^](https://github.com/concentjs/concent)

[Edit on CodeSandbox](https://codesandbox.io/s/concent-guide-xvcej)
![https://codesandbox.io/s/concent-guide-xvcej](https://user-gold-cdn.xitu.io/2019/11/17/16e79b2dee1e3c4d?w=302&h=62&f=png&s=9408)