Open source is not easy, thank you for your support, ❤ star me if you like concent ^_^
Preface
Recently, more and more of the latest state management solutions from facebook 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.
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
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
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.
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
)
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
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
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
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 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
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.
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
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.
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
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
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
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 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
// --------- 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.
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.
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.
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
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>
);
}
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
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
concentmoduleComputed
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.
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 the code example
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.
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
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.
// 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
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.
// 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.
// 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.
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
Top comments (0)