5

TNTWeb-the full name of Tencent News front-end team, the small partners in the group have practiced and accumulated in the front-end fields such as Web front-end, NodeJS development, UI design, and mobile APP.

At present, the team mainly supports the front-end development of Tencent News's various businesses. In addition to business development, it has also accumulated some front-end infrastructure to empower business efficiency and product innovation.

The team advocates open source and co-construction, and has a variety of technical experts. The team’s Github address: https://github.com/tnfe

The author of this article fantasticsoul , concent github: https://github.com/concentjs/concent

Preface

Facebook’s new new state management program recoil , the biggest highlight is the concept of atom , which manages all state nodes atomically, and then allows users to freely combine them in the logical layer.

After experiencing recoil, accurately updating 16145f0f3889af. This point will be discussed with you at the end of the following. Before that, this article mainly analyzes the differences in the experience of Concent and Recoil What is the new impact on the future development model, and what kind of changes need to be made in thinking.

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 conceptual. Representative works are unstated, constate, etc. The core code of most works may not exceed 500 Row.

At this point we have a look at Recoil should belong to? Obviously it belongs to the Context genre according to its characteristics, so the main light weight we said above
Recoil does not apply, and open source library that found hundreds of lines of code not get away, so based on Context api do well with a lightweight and powerful it may not be, this shows facebook to Recoil is ambitious and great hopes.

image.png

We also look at Concent belongs to? Concent After the v2 version, the data tracking mechanism is refactored, and the defineProperty and Proxy features are enabled, so that the react application retains the immutable pursuit and enjoys the performance improvement benefits of runtime dependency collection and precise ui update. Now that it is enabled defineProperty and Proxy, so 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 of state distribution scheduling on top of React's own rendering scheduling mechanism. The mechanism, defineProperty and Proxy are only used to assist the collection of instances and the dependence of derived data on module data, and the modified data entry is still setState (or dispatch, invoke, sync based on setState encapsulation), so that Concent can access the react application without intrusion. True plug-and-play and non-sense access.

The core principle of plug-and-play Concent 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, keeping the original setState is 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 the reactSetState of these instances in turn, and then Achieved the purpose of all the status synchronization.

r1.png

Recoil first experience

Let’s take a commonly used counter as an example to get familiar with the four high-frequency APIs exposed by Recoil

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

Define the state

External use atom interface, define a key as num , the initial value is the state 0

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

Define derived data

Use the selector interface externally, define a key as numx10 , the initial value is calculated again numState

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

Define asynchronous derived data

selector of get supports defining asynchronous functions

The point to note is that if there is a dependency, you must write the dependency first and execute the asynchronous logic at the beginning
const delay = () => new Promise(r => setTimeout(r, 1000));

const asyncNumx10Val = selector({
  key: "asyncNumx10",
  get: async ({ get }) => {
    // !!!这句话不能放在delay之下, selector需要同步的确定依赖
    const num = get(numState);
    await delay();
    return num * 10;
  }
});

Consumption status

Use the useRecoilState interface in the component to pass in the desired state (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>
  );
}

Consumption-derived data

The component uses the useRecoilValue interface to pass in the derived data (created by selector ) that you want to get. Both synchronous derived data and asynchronous derived data 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

defined two components, 16145f0f388d17 view online example

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

The top-level node wraps React.Suspense and RecoilRoot , the former is used to meet the needs of asynchronous calculation functions, and the latter is used to inject the 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
);

a2.gif

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 the ui and directly complete the purpose of defining the state & modifying the state

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

run({
  counter: {// 声明一个counter模块
    state: { num: 1 }, // 定义状态
  }
});

console.log(getState('counter').num);// log: 1
setState('counter', {num:10});// 修改counter模块的num值为10
console.log(getState('counter').num);// log: 10

We can see that this redux . A single state tree needs to be defined, and the first-level key guides the user to modularize the management of the data.

Introduce reducer

In the above example, we directly call setState modify the data, but the real situation is that there are many synchronous or asynchronous business logic operations before the data falls, so we add the reducer definition to the module to declare the set of methods to modify the data.

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

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

const state = () => ({ num: 1 });// 状态声明
const reducer = {// reducer声明
  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 will return a Promise, we need to wrap it up with an async to execute the code
import { dispatch } from "concent";

(async ()=>{
  console.log(getState("counter").num);// log 1
  await dispatch("counter/inc");// 同步修改
  console.log(getState("counter").num);// log 2
  await dispatch("counter/asyncInc");// 异步修改
  console.log(getState("counter").num);// log 3
})()

Note that the dispatch call is based on the string matching method. The reason for retaining this call method is to take care of the scenes that need to be dynamically called. In fact, the more recommended way of writing is

import { dispatch } from "concent";

await dispatch("counter/inc");
// 修改为
await dispatch(reducer.inc);

In fact reducer collection defined by the run interface has been concent , and allows users to reducer.${moduleName}.${methodName} in the manner of 06145f0f388ed6, so here we can even initiate calls reducer

import { reducer as ccReducer } from 'concent';

await dispatch(reducer.inc);
// 修改为
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 class component is a concent component
  • useConcent, register the function component as a 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 the difference between the two writing methods is very small, except for the different definitions of components, 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
);

a3.gif

Comparing with Recoil , we found that there is no top layer and there is no Provider or Root . The react component has been connected to the concent, achieving true plug-and-play and non-perceptual access. At the same time, api kept in the react .

Component calls reducer

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

mr is short for moduleReducer, and it is legal to write it directly as ctx.moduleReducer
//  --------- 对于类组件 -----------
changeNum = () => this.setState({ num: 10 })
// ===> 修改为
changeNum = () => this.ctx.mr.inc(10);// or this.ctx.mr.asynInc(10)

//当然这里也可以写为ctx.dispatch调用,不过更推荐用上面的moduleReducer直接调用
//this.ctx.dispatch('inc', 10); // or this.ctx.dispatch('asynInc', 10)

//  --------- 对于函数组件 -----------
const { state, mr } = useConcent("counter");// useConcent 返回的就是ctx
const changeNum = () => mr.inc(20); // or ctx.mr.asynInc(10)

//对于函数组将同样支持dispatch调用方式
//ctx.dispatch('inc', 10);  // or ctx.dispatch('asynInc', 10)

Asynchronous calculation function

run interface supports the extended computed attribute, which allows users to define a set of calculation functions for a bunch of derived data. They can be synchronous or asynchronous, and support one function to use the output of another function as input for secondary calculations. The input dependencies of the calculation are automatically collected.

 const computed = {// 定义计算函数集合
  numx10({ num }) {
    return num * 10;
  },
  // n:newState, o:oldState, f:fnCtx
  // 结构出num,表示当前计算依赖是num,仅当num发生变化时触发此函数重计算
  async numx10_2({ num }, o, f) {
    // 必需调用setInitialVal给numx10_2一个初始值,
    // 该函数仅在初次computed触发时执行一次
    f.setInitialVal(num * 55);
    await delay();
    return num * 100;
  },
  async numx10_3({ num }, o, f) {
    f.setInitialVal(num * 1);
    await delay();
    // 使用numx10_2再次计算
    const ret = num * f.cuVal.numx10_2;
    if (ret % 40000 === 0) throw new Error("-->mock error");
    return ret;
  }
}

// 配置到counter模块
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 errorHandler in the second options configuration of the run interface to capture it.

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

Of course, it is better to use the concent-plugin-async-computed-status plug-in 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], // 配置异步计算函数执行状态管理插件
  }
);

The plug-in will automatically configure a cuStatus module to concent, which is convenient for components 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",// 属于counter模块,状态直接从state获得
    connect: ["cuStatus"],// 连接到cuStatus模块,状态从connectedState.{$moduleName}获得
  });
  const changeNum = () => setState({ num: state.num + 1 });
  
  // 获得counter模块的计算函数执行状态
  const counterCuStatus = connectedState.cuStatus.counter;
  // 当然,可以更细粒度的获得指定结算函数的执行状态
  // const {['counter/numx10_2']:num1Status, ['counter/numx10_3']: num2Status} = connectedState.cuStatus;

  return (
    <div>
      {state.num}
      <br />
      {counterCuStatus.done ? moduleComputed.numx10 : 'computing'}
      {/** 此处拿到错误可以用于渲染,当然也抛出去 */}
      {/** 让ErrorBoundary之类的组件捕捉并渲染降级页面 */}
      {counterCuStatus.err ? counterCuStatus.err.message : ''}
      <br />
      {moduleComputed.numx10_2}
      <br />
      {moduleComputed.numx10_3}
      <br />
      <button onClick={changeNum}>changeNum</button>
    </div>
  );
}

a4.gif

Accurate update

I told the opening Recoli mentioned precise update maintained a skeptical attitude, there are some misleading suspects, where we will reveal the mystery

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 ui rendered in a state with less than if this data somewhere changed num value will still trigger NumView re-rendering, but concent examples taken out of context in state and moduleComputed is a Proxy object in real time The collection of dependencies required for each round of rendering, this is the true meaning of on-demand rendering and accurate updates.

const NumView = () => {
  const [show, setShow] = useState(true);
  const {state} = useConcent('counter');
  // show为true时,当前实例的渲染对state.num的渲染有依赖
  return {show ? <h1>{state.num}</h1> : 'nothing'}
}

a5.gif

Click me to view the code sample

Of course, if the user needs to do other things when the num value has been rendered after the ui is changed, similar to useEffect , concent also supports users to setup it into 06145f0f3891fd, and define effect to complete this scene request, compared to useEffect ctx.effect in the 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 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/>
}

more information about effect and useEffect, please see this article

current mode

Regarding the question whether concent supports current mode , let’s talk about the answer first. concent is 100% fully supported, or further, all state management tools will eventually trigger setState or forceUpdate . We just don’t need to write anything during the rendering process. The side-effect code, the power of the rendering result obtained by the same state input, is a safe code to run current mode

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

// bad
function Test(){
   track.upload('renderTrigger');// 上报渲染触发事件
   return <h1>bad case</h1>
}

// good
function Test(){
   useEffect(()=>{
      // 就算仅执行了一次setState, current mode下该组件可能会重复渲染,
      // 但react内部会保证该副作用只触发一次
      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 gives React the to schedule the rendering process of the component component 16145f0f3892c4 as the unit, which can hover and Enter the rendering again, arrange the higher priority to render first, the heavily rendered components will be slices for multiple time periods and repeated rendering, and the context of the concentration itself is independent of the existence of react (access to the concentration does not need to wrap any Provider at the top level ), 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 and establishing the dependent store data), and then react’s own scheduling process, The function to modify 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 only schedules the rendering timing of the component, and the component's Interrupts and for this rendering process.

So the following sample code is no problem

const setup = (ctx)=>{
  ctx.effect(()=>{
     // effect是对useEffect的封装,
     // 同样在current mode下该副作用也只触发一次(由react保证)
      track.upload('renderTrigger');
  });
}

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

Similarly, in the current mode 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.

// 假设这是一个渲染很耗时的组件,在current mode模式下可能会被中断渲染
function HeavyComp(){
  const { state } = useConcent({module:'counter'});// 属于counter模块

 // 这里读取了num 和 numBig两个值,收集到了依赖
 // 即当仅当counter模块的num、numBig的发生变化时,才触发其重渲染(最终还是调用setState)
 // 而counter模块的其他值发生变化时,不会触发该实例的setState
  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 one more layer of work to guide users to strip the logic to their rules. Finally, the business processing data is returned to the react component to call its setState or forceUpdate trigger re-rendering. current mode will not have any impact on the existing state management or new state management solutions, but only on the user’s UI The code puts forward higher requirements, so as current mode cause bugs that are difficult to eliminate because of 06145f0f38933a

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 in line with the specification React code in order to adapt to the current mode provided in the future.

All new features of react are fiber activated by 06145f0f389391. With the fiber architecture, hook , time slicing , suspense and the future Concurrent Mode . Class components and function components can work Concurrent Mode under 06145f0f389398, as long as they follow the specifications.

from: 16145f0f3893ac 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 Concurrent Mode . First let users get used to these restrictions, step by step, and finally launch Concurrent Mode .

Concluding remarks

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 even after the code is large.

// 定义状态
const numState = atom({key:'num', default:0});
const numBigState = atom({key:'numBig', default:100});
// 定义衍生数据
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处消费状态或衍生数据
const [num] = useRecoilState(numState);
const [numBig] = useRecoilState(numBigState);
const numx2 = useRecoilValue(numx2Val);
const numBigx2 = useRecoilValue(numBigx2Val);
const numSumBig = useRecoilValue(numSumBigVal);

Concent follows redux single state tree, respects modular management data and derived data, and relies on the Proxy to complete the runtime dependency collection and pursue the perfect integration of the

run({
  counter: {// 声明一个counter模块
    state: { num: 1, numBig: 100 }, // 定义状态
    computed:{// 定义计算,参数列表里解构具体的状态时确定了依赖
       numx2: ({num})=> num * 2,
       numBigx2: ({numBig})=> numBig * 2,
       numSumBig: ({num, numBig})=> num + numBig,
     }
  },
});

And class components and functions can use same way as to consume data and bind methods

// ###### 函数组件
function Demo(){
  const { state, moduleComputed, setState } = useConcent('counter') 
  // ---> ui处消费状态或衍生数据,在ui处结构了才产生依赖
  const { numx2, numBigx2, numSumBig} = moduleComputed;
  const { num, numBig } = state;
  // ... ui logic
}

// ###### 类组件
const DemoCls = register('counter')(
  class DemoCls extends React.Component{
   render(){
      const { state, moduleComputed, setState } = this.ctx; 
      // ---> ui处消费状态或衍生数据,在ui处结构了才产生依赖
      const { numx2, numBigx2, numSumBig} = moduleComputed;
      const { num, numBig } = state;
      // ... ui logic
    }
  }
)

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
  • Class components and function components can share a set of model models
  • Support middleware and plug-in mechanism, it is easy to be compatible with redux ecology
  • At the same time, it supports centralized and fractal module configuration, synchronous and asynchronous module loading, which is more friendly to the flexible reconstruction process of large-scale projects

❤ star me if you like concent ^_^

team

TNTWeb-Tencent news front-end team, TNTWeb is committed to the industry's cutting-edge technology exploration and the improvement of team members' personal capabilities. The latest high-quality content in the field of small programs and web front-end technology has been compiled for front-end developers, updated weekly✨, welcome to star, github address: https://github.com/tnfe/TNT-Weekly

logo.png


TNTWEB
3.8k 声望8.5k 粉丝

腾讯新闻前端团队