TNTWeb – The full name of Tencent news front end team, partners in the group have practiced and accumulated experience in Web front end, NodeJS development, UI design, mobile APP and other large front end fields.

At present, the team mainly supports the front-end development of Tencent news business. Besides business development, some front-end infrastructure has been accumulated to enable business efficiency improvement and product innovation.

The team advocates open source construction, has a variety of technical masters, the team Github address: github.com/tnfe

In this paper, the author fantasticsoul, concent github:github.com/concentjs/c…

The preface

Recoil, Facebook’s new status management solution, highlights the concept of Atom, which manages all status nodes atomically and then lets users combine them in a logical layer.

After playing With Recoil, I was a little skeptical about the accuracy of the update, which I’ll discuss at the end of this article. The main focus of this article is to analyze the differences between The experience of Using Concent and Recoil, and predict how they will affect future development models and what kind of changes are needed in thinking.

Three major schools of data flow schemes

At present, the mainstream data flow schemes can be divided into the following three types according to their forms

  • Story genres

Redux, and other works based on Redux, and works similar to Redux, such as DVA, Rematch, and so on.

  • Mobx genre

DefinePerperty and Proxy do data hijacking to achieve responsive programming, mobx-like works, such as DOB, etc.

  • The Context genre

Context here refers to the Context API that comes with React. Data flow schemes based on Context API are usually lightweight, easy to use and have few concepts, which means works such as unstated and constate. Most works may have less than 500 lines of core code.

Where does Recoil fall into this category? If you open up the source code base and find that the code is not hundreds of lines long, then doing something nice and powerful based on the Context API is not necessarily lightweight. This shows that Facebook has ambitions and high hopes for Recoil.

Let’s also see what category Concent fits into. After the V2 release, Concent refactored the data tracking mechanism to enable defineProperty and Proxy features, allowing react applications to retain the immutable features and enjoy the performance benefits of runtime dependency collection and precise UI updates. Now that defineProperty and Proxy are enabled, does it look like Concent belongs to the Mobx genre?

In fact, Concent is a brand new genre. It does not rely on react Context API, does not destroy the form of react component itself, and maintains the philosophy of pursuing immutability. It only establishes a logical layer state distribution scheduling mechanism based on react’s own rendering scheduling mechanism. DefineProperty and Proxy are only used to assist in collecting instance and derived data dependencies on module data, while modifying data entry is still setState(or Dispatch, Invoke, sync based on setState encapsulation), Allows Concent to access the React app with 0 intrusion, true plug and play and senseless access.

The core principle of plug and play is that Concent builds a global context parallel to the React runtime, carefully maintains the ownership relationship between the module and the instance, and takes over the update entry setState of the component instance, leaving the original setState as reactSetState. So when the user calls setState, Concent not only calls reactSetState to update the UI of the current instance, but also intelligently determines whether there are other instances of the submitted state that care about its change, and then executes the reactSetState of these instances successively. Then achieve the purpose of all state synchronization.

At the beginning of Recoil experience

Using the common counter as an example, let’s familiarize ourselves with four frequently used apis exposed by Recoil

  • Atom, defines state
  • Selector, defines derived data
  • UseRecoilState, consumption status
  • UseRecoilValue, consumes derived data

Define state

Externally, the atom interface is used to define a state whose key is num and initial value is 0

const numState = atom({
  key: "num".default: 0
});
Copy the code

Defining derived data

Externally, the selector interface is used to 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; }});Copy the code

Define asynchronous derived data

The selector get allows you to define asynchronous functions

It is important to note that if there are dependencies, they must be written before the asynchronous logic is executed

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

const asyncNumx10Val = selector({
  key: "asyncNumx10".get: async ({ get }) => {
    / /!!!!!! This statement cannot be placed under delay, because selector requires a definite dependency of synchronization
    const num = get(numState);
    await delay();
    return num * 10; }});Copy the code

Consumption status

The component uses the useRecoilState interface to pass in the desired state (created from Atom)

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

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

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

Consuming derived data

The component uses the useRecoilValue interface, which passes in derived data (created by selector) that it wants to retrieve, both synchronous and asynchronous derived data

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

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

Render them to see the results

Expose the two defined components for an online example

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

The top layer nodes wrap React.Suspense and RecoilRoot, the former for asynchronous computation functions and the latter for injecting Recoil contexts

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

Experience in the early Concent

If you read the Concent documentation (still under construction…) Some people may think that there are too many apis to remember, but most of them are optional syntax candies. Take Counter as an example, we only need to use the following two apis

  • Run, define module state (required), module calculation (optional), module observation (optional)

After running the Run interface, a Concent global context is generated

  • SetState: Indicates the modification status

Define state & Modify state

In the following example, we will leave the UI and go straight to the purpose of defining state & modifying 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});// Change the num value of the counter module to 10
console.log(getState('counter').num);// log: 10
Copy the code

As you can see, here, much like Redux, you need to define a single state tree, and the first level key guides the user to manage the data modularized.

The introduction of reducer

In the above example, we directly called setState to modify data, but the real situation is that there are many synchronous or asynchronous business logic operations before data landing. Therefore, we added reducer definition to the module to declare the collection of methods to modify data.

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

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

const state = () = > ({ num: 1 });// Status declaration
const reducer = {/ / reducer statement
  inc(payload, moduleState) {
    return { num: moduleState.num + 1 };
  },
  async asyncInc(payload, moduleState) {
    await delay();
    return { num: moduleState.num + 1}; }}; run({counter: { state, reducer }
});
Copy the code

We then use Dispatch to trigger the method to change the state

Since dispatch returns a Promise, we need to wrap an async around the code to execute it

import { dispatch } from "concent";

(async() = > {console.log(getState("counter").num);// log 1
  await dispatch("counter/inc");// Synchronize the changes
  console.log(getState("counter").num);// log 2
  await dispatch("counter/asyncInc");// Asynchronous change
  console.log(getState("counter").num);// log 3}) ()Copy the code

Note that the dispatch call is based on string matching. The reason why this call is reserved is to accommodate the scenario that requires dynamic invocation. In fact, the more recommended method is

import { dispatch } from "concent";

await dispatch("counter/inc");
/ / changed to
await dispatch(reducer.inc);
Copy the code

In fact, the Reducer collection defined by the Run interface has been centrally managed by Concent and allowed to be called by reducer.${moduleName}.${methodName}, so we can even call from reducer

import { reducer as ccReducer } from 'concent';

await dispatch(reducer.inc);
/ / changed to
await ccReducer.counter.inc();
Copy the code

Access the react

The react component generates instance context (equivalent to the render context mentioned in vue 3 Setup) and the ability to consume data from the Concent module

  • Register: The concent component is registered
  • UseConcent, the registered function component is the 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>
  );
}
Copy the code

Note that there is little difference between the two methods, except for the way the components are defined, the rendering logic and data source are identical.

Render them to see the results

Online sample

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

Compared with Recoil, we found that without the top-level component package like Provider or Root, the React component was connected to Concent, realizing real plug and play and perception-free access. Meanwhile, the API was written in the same way as react.

Component Call Reducer

Concent generates instance context for each component instance, so that users can directly call the Reducer method through ctx.mr

Mr Is short for moduleReducer, directly written as CTX. ModuleReducer is also legal

// --------- for the class component -----------
changeNum = () = > this.setState({ num: 10 })
// ===> Is changed to
changeNum = () = > this.ctx.mr.inc(10);// or this.ctx.mr.asynInc(10)

// This can also be called ctx.dispatch, but it is recommended to use the above moduleReducer directly
//this.ctx.dispatch('inc', 10); // or this.ctx.dispatch('asynInc', 10)

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

// Dispatch calls will also be supported for groups of functions
//ctx.dispatch('inc', 10); // or ctx.dispatch('asynInc', 10)
Copy the code

Asynchronous computation function

The RUN interface supports extending computed attributes, which allow users to define a collection of computations with derived data, either synchronous or asynchronous, and to allow one function to perform a second computation using the output of another function as input, with input dependencies automatically collected.

 const computed = {// Define a set of computing functions
  numx10({ num }) {
    return num * 10;
  },
  // n:newState, o:oldState, f:fnCtx
  If num is changed, the recalculation of this function is triggered
  async numx10_2({ num }, o, f) {
    // setInitialVal must be called to give numx10_2 an initial value,
    // This function is executed only once on 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 count again
    const ret = num * f.cuVal.numx10_2;
    if (ret % 40000= = =0) throw new Error("-->mock error");
    returnret; }}// Configure to counter module
run({
  counter: { state, reducer, computed }
});
Copy the code

In the above calculation function, we deliberately set numx10_3 to 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.

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

Of course, it is better to use the concent-plugin-async-computed-status plug-in to achieve 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 computation function to execute the state management plug-in});Copy the code

The plug-in automatically configudes a cuStatus module to Concent, making it easy for components to connect to it and consume data about the execution status of related computation functions

function Test() {
  const { moduleComputed, connectedState, setState, state, ccUniqueKey } = useConcent({
    module: "counter".// Belongs to the counter module, and the state is obtained directly from state
    connect: ["cuStatus"].// Connect to the cuStatus module with the status obtained from connectedState.{$moduleName}
  });
  const changeNum = () = > setState({ num: state.num + 1 });
  
  // Get the state of the counter module's calculation function execution
  const counterCuStatus = connectedState.cuStatus.counter;
  Of course, it is possible to obtain the execution status of the specified settlement function at a finer level
  // const {['counter/numx10_2']:num1Status, ['counter/numx10_3']: num2Status} = connectedState.cuStatus;

  return (
    <div>
      {state.num}
      <br />{counterCuStatus.done ? moduleComputed.numx10 : 'Computing '} {/** Here the error can be used for rendering and of course thrown out */} {/** Let a component like ErrorBoundary catch 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>
  );
}
Copy the code

Accurate update

In the beginning, I said that I was skeptical of Recoli’s precise updates, and that there was some suspicion of misdirection, which we will address here

We know 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); }}Copy the code

However, the state and moduleComputed taken out of concent’s instance context are Proxy objects, which are used to collect the dependencies needed for each round of rendering in real time. This is true rendering on demand and accurate updating.

const NumView = () = > {
  const [show, setShow] = useState(true);
  const {state} = useConcent('counter');
  // when show is true, the current instance's rendering is dependent on state.num's rendering
  return {show ? <h1>{state.num}</h1> : 'nothing'}}Copy the code

Click on me to see code examples

Of course, if the user needs to do something else when num value changes after UI rendering, like useEffect, Concent also allows users to pull it into setup and define effect to complete the scene. Compared to useEffect, Ctx. effect in setup only needs to be defined once, with the key name passed, and Concent will automatically compare the previous and current values to determine 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/>
}
Copy the code

See this article for more information about effect and useEffect

current mode

Concent has 100% support for current mode, or more specifically, all state management tools eventually trigger setState or forceUpdate. As long as we don’t write code with any side effects during the rendering process, let the same state input get the power of the rendering result, i.e. safe code to run in current mode.

Current Mode just makes more demands on our code.

// bad
function Test(){
   track.upload('renderTrigger');// Report the render 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,
      // React ensures that this side effect is triggered only once
      track.upload('renderTrigger');
   })
   return <h1>bad case</h1>
}

Copy the code

First, we need to understand the principle of Current mode because fiber simulates the entire render stack (i.e., the information stored on fiber nodes). This gives react the opportunity to schedule component rendering on a per-component basis, hover and re-enter the render, and assign the higher-priority render first. Heavily rendered components will be sliced into multiple periods of repeated rendering, and the context of Concent itself exists independently of React (there is no need to package any Provider on the top layer to access Concent). Concent is only responsible for business processing and generating new data, and then sending it to corresponding instances as needed (instance states themselves are isolated islands. Concent is only responsible for synchronizing dependent store data), and then react’s own scheduling process. The function to change the state will not be executed multiple times due to component re-entrant (this requires us to follow the principle that we should not write code with side effects during rendering). React is simply a time to schedule component rendering. Component interrupts and reentrant targets are also part of the rendering process.

So the following sample code is fine

const setup = (ctx) = >{
  ctx.effect(() = >{
     // effect encapsulates useEffect,
     // React only triggers this side effect once in current mode.
      track.upload('renderTrigger');
  });
}

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

Copy the code

Similarly, dependent collection in current mode, repeated rendering simply causes multiple collections to be triggered. As long as the state input is the same, the rendered result is idempotent and the collected dependent result is idempotent.

// Assume that this is a component that takes a lot of time to render and may be interrupted in current mode
function HeavyComp(){
  const { state } = useConcent({module:'counter'});// Belongs to the counter module

 NumBig = num; numBig = numBig
 // Rerender the counter module only when num and numBig change (setState is called eventually)
 // When other values of the counter module change, the setState of the instance is not triggered
  return (
    <div>num: {state.num} numBig: {state.numBig}</div>
  );
}
Copy the code

Finally, the hooks themselves support stripping logic into custom hooks (functions that do not return from the UI), while other state management is just another layer of work that guides users to strip logic into their rules. The react component calls its setState or forceUpdate to trigger rerendering. The introduction of current mode will not have any impact on the existing state management or the new state management scheme, but only put forward higher requirements for user UI code. Avoid bugs that are hard to eliminate due to current mode

React also provides a react. Strict component to intentionally trigger the double-call mechanism, reactjs.org/docs/strict… In order to guide users to write more compliant React code for future provided Current mode.

React all the new features are actually enabled by Fiber. Fiber architecture has spawned Hook, Time Slicing, Suspense, and the future Concurrent Mode. Both class and function components can work safely in Concurrent Mode, as long as the specification is followed.

Excerpt from: reactjs.org/docs/strict…

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

Strict is a secondary API designed to guide users to write code that can run in Concurrent Mode. It allows users to get used to these limitations and then pushes Concurrent Mode further.

conclusion

Recoil advocates more fine-grained control over state and derived data, which looks simple when written as a demo, but is actually cumbersome when the code is large.

// Define the 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),
});

// --> UI consumption state or derived data
const [num] = useRecoilState(numState);
const [numBig] = useRecoilState(numBigState);
const numx2 = useRecoilValue(numx2Val);
const numBigx2 = useRecoilValue(numBigx2Val);
const numSumBig = useRecoilValue(numSumBigVal);
Copy the code

Concent follows the nature of Redux’s single-state tree, advocating modular management of data and derived data, while relying on Proxy capabilities for runtime dependency collection and immutable integration.

run({
  counter: {// Declare a counter module
    state: { num: 1.numBig: 100 }, // Define the state
    computed: {// Define the calculation, which determines the dependency when deconstructing the specific state in the argument list
       numx2: ({num}) = > num * 2.numBigx2: ({numBig}) = > numBig * 2.numSumBig: ({num, numBig}) = > num + numBig,
     }
  },
});

Copy the code

Class components and functions can consume data and bind methods in the same way

// ###### function component
function Demo(){
  const { state, moduleComputed, setState } = useConcent('counter') 
  // --> UI consumption state or derived data, in the UI structure to generate dependencies
  const { numx2, numBigx2, numSumBig} = moduleComputed;
  const { num, numBig } = state;
  // ... ui logic
}

// ###### class component
const DemoCls = register('counter') (class DemoCls extends React.Component{
   render(){
      const { state, moduleComputed, setState } = this.ctx; 
      // --> UI consumption state or derived data, in the UI structure to generate dependencies
      const { numx2, numBigx2, numSumBig} = moduleComputed;
      const { num, numBig } = state;
      // ... ui logic}})Copy the code

So you get:

  • Runtime dependency collection also follows the react immutable principle
  • All functions (State, Reducer, computed, Watch, Event…) , can get more friendly TS support
  • Class components and function components can share a set of Models
  • Support for middleware and plug-in mechanisms, easily compatible with the Redux ecosystem
  • It also supports centralized and fractal module configuration, synchronous and asynchronous module loading, which is more friendly to the elastic reconstruction process of large projects

❤ star me if you like concent ^_^

team

TNTWeb – Tencent news front end team, TNTWeb is committed to the exploration of cutting-edge technology in the industry and the improvement of team members’ personal ability. For front-end developers to organize the applets and the latest quality content in the field of web front-end technology, weekly update ✨, welcome star, github address: github.com/tnfe/TNT-We…