Slightly talk about Concent & Recoil and explore the new development model of react data flow

Posted Jun 15, 202013 min read

rvc3.png

Open source is not easy, thank you for your support, star me if you like concent ^_^

Preface

Previously published an article redux, mobx, concentrate feature competition, see how the next generation matches the predecessor , attracting many interested friends to join the group and start to understand And use concent , and get a lot of positive feedback, which really helped them improve their development experience. Although the number of people in the group is still small, everyone is enthusiastic Soaring, strong technical discussion atmosphere, and maintaining a certain sensitivity to many fresh technologies, such as the latest state management program from Facebook that was gradually mentioned more and more from last month [recoil]( https://github . com/facebookexperimental/Recoil), although it is still in the experimental state, but everyone must have begun to try it privately. After all, they are born with famous names and endorsed by fb, they will definitely shine.

However, after I experienced the recoil, I remained skeptical about the accurate updates advertised in it, and there are some misleading suspicions. This will be analyzed separately below. Whether it is misleading readers can naturally come to a conclusion after reading this article. In short, this article mainly analyzes the differences in the code styles of Concent and Recoil, and discusses their new impact on our future development model, and what kind of changes in thinking need to be done.

Three genres of data flow solutions

At present, the mainstream data flow solutions can be divided into the following three categories according to form.

  • redux genre

Redux, and other works based on redux, as well as works similar to redux ideas, representative works are dva, rematch, etc.

  • mobx genre

With the help of definePerperty and Proxy to complete data hijacking, so as to achieve the goal of responsive programming, there are many works like mobx, such as dob and so on.

  • Context genre

Context here refers to the Context api that comes with react. The data flow scheme based on the Context api is usually light-weight, easy to use, and has few overviews. The representative works include unstated and conversation. The core code of most works may not exceed 500. Row.

Now let's see what kind of Recoil should belong to? Obviously it belongs to the Context genre according to its characteristics, so the main lightweight we mentioned above is right
Recoil doesn t apply anymore. Opening its source code library reveals that the code is not a few hundred lines, so it is not necessarily lightweight based on Context api to be easy to use and powerful. From this, it can be seen that facebook vs. Recoil There is ambition and great hope.

Let's also take a look at what category Concent belongs to? After the v2 version of Concent, the data tracking mechanism was refactored, and the defineProperty and Proxy features were enabled to allow the react application to retain the immutable pursuit and enjoy the performance improvement benefits of relying on collection and accurate ui update at runtime. , Since defineProperty and Proxy are enabled, then it seems that Concent should belong to the mobx genre?

In fact, Concent belongs to a brand-new genre, 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 builds a logical layer state on top of React s own rendering scheduling mechanism Distribution scheduling mechanism, defineProperty and Proxy are only used to assist in the collection of instance and derived data dependence on module data, and the modified data entry is setState(or dispatch, invoke, sync based on setState encapsulation), so that Concent can be accessed by 0 intrusion Into the react application, true plug and play and non-sense access.

Plug and Play The core principle is that Concent has built a global context parallel to the react runtime, carefully maintains the attribution relationship between this module and the instance, and also takes over the update entry setState of the component instance , Retain the original setState as reactSetState, so when the user calls setState, in addition to calling reactSetState to update the current instance ui, and intelligently determine whether there are other instances of the submitted state that care about its changes, and then take it out and execute these instances in turn ReactSetState, and then achieve the purpose of all state synchronization.

Recoil first experience

Let's take the commonly used counter as an example to familiarize yourself with the four high-frequency APIs exposed by Recoil

  • atom, define the state
  • selector, define derived data
  • useRecoilState, consumption state
  • useRecoilValue, consumption derived data

Define status

Use the atom interface externally to define a state where the key is num and the initial value is 0

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

Define derived data

Externally use the selector interface, define a key as numx10, and the initial value is calculated again depending on numState

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

Define asynchronous derived data

select of get supports defining asynchronous functions

The point to note is that if there is a dependency, you must first write the dependency and start executing asynchronous logic

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 determined synchronously
    const num = get(numState);
    await delay();
    return num * 10;
  }
});

Consumption status

Use the useRecoilState interface in the component, passing in the state you want to get(created by atom)

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

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

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

Consumer derived data

Use the useRecoilValue interface in the component, pass in the derived data you want to get(created by selector), synchronously derived data and asynchronously derived data, both can be obtained through this interface

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

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

Render them to see the results

Expose the two components defined, [View online example]( https://codesandbox.io/s/recoiljs-is-meant-to-rock-your-react-world-oljjf?file=/src/List . jsx:636-730)

export default()=>{
  return(
    <>
      <NumView />
      <NumValView />
    </>
 );
};

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

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

);

Concent first experience

If you have read the concent document(still under construction...), some people may think that there are too many APIs, which is difficult to remember. In fact, most of them are optional syntactic sugar. We take counter as an example, and only need to use The following two APIs are enough

  • run, define module status(required), module calculation(optional), module observation(optional)

After running the run interface, a concentrated global context will be generated

  • setState, modify the state

Define status & modify status

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

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

run({
  counter:{//Declare a counter module
    state:{num:1 }, //define the 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 here is very similar to redux, you need to define a single state tree, and at the same time, the first layer of key guides the user to modularize the data management.

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 hits the ground, so we fill the module with the definition of reducer, which is used to declare the modified data Method collection.

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 to modify the state

Because dispatch returns a Promise, we need to wrap it with an async to execute the code

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");//modify asynchronously
console.log(getState("counter").num);//log 3
})()

Note that the dispatch method is based on the string matching method. The reason for retaining this calling method is to take care of scenarios that require dynamic calling. In fact, the more recommended way of writing is

import {dispatch} from "concent";

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

Access 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 an instance context(equivalent to the rendering context mentioned in vue 3 setup), and obtain the consumption cent module Data capabilities

  • register, register the class component as a concent component

  • useConcent, register function component as concent component

    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 there is very little difference between the two writing methods, except that the definition of components is different, in fact, the rendering logic and data sources are exactly the same.

Render them to see the results

Online example

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <div>
      <ClsComp />
      <FnComp />
    </div>
  </React.StrictMode>,
  rootElement

);

Comparing with Recoil, we found that there is no top layer and no package like Provider or Root, the react component is already connected to the concent, achieving true plug-and-play and non-aware access, while the api is reserved It is written in accordance with react.

component calls reducer

Concent generates an instance context for each component instance, which is convenient for users to call the reducer method directly through ctx.mr

mr is shorthand for moduleReducer, it is legal to write directly as ctx.moduleReducer

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

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

Asynchronous calculation function

The run interface supports the extension of the computed attribute, which allows the user to define a collection of calculation functions for derived data, which can be either synchronous or asynchronous, while supporting the use of the output of another function as input In the second calculation, the input dependence of the calculation is automatically collected.

 const computed = {//Define the set of calculation functions
  numx10({ num }) {
    return num * 10;
  },
  //n:newState, o:oldState, f:fnCtx
  //num structure, indicating that the current calculation depends on num, only trigger this function recalculation when num changes
  async numx10_2({ num }, o, f) {
    //Must call setInitialVal to give numx10_2 an initial value,
    //This function is only executed once when the first computed trigger
    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 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 capture it by defining errorHandler in the second options configuration of the run interface.

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

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

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

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

);

The plug-in will automatically configure a cuStatus module to concent to facilitate the component to connect to it and consume the execution status data of related calculation functions

function Test() {
  const {moduleComputed, connectedState, setState, state, ccUniqueKey} = useConcent({
    module:"counter",//belongs to the counter module, the state is directly obtained 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, you can get the execution status of the specified settlement function at a finer granularity
  //const {['counter/numx10_2']:num1Status, ['counter/numx10_3']:num2Status} = connectedState.cuStatus;

  return(
    <div>
      {state.num}
      <br />
      {counterCuStatus.done? moduleComputed.numx10:'computing'}
      {/** The error you get here can be used for rendering, but of course throw it away */}
      {/** Let components like ErrorBoundary capture and render downgraded pages */}
      {counterCuStatus.err? counterCuStatus.err.message:''}
      <br />
      {moduleComputed.numx10_2}
      <br />
      {moduleComputed.numx10_3}
      <br />
      <button onClick={changeNum}>changeNum</button>
    </div>
 );
}

![] https://raw.githubusercontent... )

View online example

Exact update

At the beginning, I said that I remained skeptical about the accurate update mentioned by Recoli, and there are some misleading suspicions. Here we will uncover the doubt

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

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

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

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'}
}

Click me to view code examples

Of course, if the user has ui rendering of the num value, there is a need to do other things when the change occurs. Similar to the effect of useEffect, concent also supports the user to draw it into the setup and define the effect to complete this. Scene demand, compared to useEffect, ctx.effect in setup only needs to be defined once, and only the key name needs to be passed. Concent will automatically compare the value of the previous moment and the current moment to decide whether to trigger the side effect function.

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/>
}

For more information about effect and useEffect, please check this article

Conclusion

Recoil respects more fine-grained control of state and derived data. The writing of the demo looks simple. In fact, the code is still very cumbersome after a large scale.

Concent follows the essence of the redux single state tree, respects modular management data and derived data, and relies on the ability of Proxy to complete the perfect integration of run-time dependent collection and pursuit of immutability.

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 ecosystem
  • Support both centralized and fractal module configuration, synchronous and asynchronous module loading, more friendly to the elastic reconstruction process of large projects

Finally, to answer questions about whether concent supports current mode, first come to the conclusion, 100%support.

We must first understand the principle of current mode because the fiber architecture simulates and the entire rendering stack(ie, the information stored on the fiber node), so that we have the opportunity to allow React to schedule the rendering process of the component in units of components. Enter the rendering again, arrange for the first priority to render first, the heavily rendered components will be sliced into multiple time periods for repeated rendering, and the context of the concentrate itself is independent of the existence of react(access to concent does not require any top layer to wrap any Provider), It is only responsible for processing the business to generate new data, and then dispatch it to the corresponding instance as needed. After that, it will react to its own scheduling process. The function that modifies the state will not be executed multiple times because the component repeatedly reenters We follow the principle that we should not write code that contains side effects during the rendering process), react is only to schedule the rendering time of the component.

star me if you like concent ^_^

Edit on CodeSandbox
https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz
https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

If you have any questions about concent, you can scan the code and add group consultation, will try to answer the questions and help you understand more, many of the small partners have become old drivers, and they are very happy after using it. You will know when you try the boat ?.