Compare redux, mobx and concent in depth to let you have a funny way in developing react app.
幻魂
Posted on April 13, 2020
Compare redux, mobx and concent in depth to let you have a funny way in developing react app.
❤ star me if you like concent ^_^
Preface
redux
andmobx
are themselves independent state management frameworks, each with its own abstract api, which has nothing to do with other UI frameworks (react, vue ...), this article mainly talks about the contrast effect of using it with react
, So the redux
andmobx
mentioned below imply react-redux
andmobx-react
which are binding libraries that allow them to function in react
, andconcent
itself is for React
personalized development framework, data flow management is only one of the functions, and the additional features that enhance the development experience of React can be used as needed. Later, all the parts related toreact
in concent
will be cut away Release concent-core
, its positioning is similar toredux
and mobx
.
So the players who will appear in this article are
redux & react-redux
slogan
JavaScript state container, providing predictable state managementdesign concept
Single data source, use pure functions to modify state
mobx & mobx-react
slogan:
Simple and scalable state managementdesign concept
Anything that can be derived from the application state should be derived
concent
slogan:
Predictable, zero-invasion, progressive, high-performance react development solutiondesign concept
Believe that the development method of integrating immutable + dependent collection is the future of react, enhancing the characteristics of react components, writing less and doing more.
After introducing the background of the three, our stage is officially handed over to them, and we start a round of competition. Who will be your favorite one in the end?
结果预览
The following five contest rounds have more actual demo codes. Here, the comparison results will be notified in advance, so that the readers can quickly understand.
store configuration | concent | mobx | redux |
---|---|---|---|
Support separation | Yes | Yes | No |
No root Provider & use without explicit import | Yes | No | No |
reducer without this
|
Yes | No | Yes |
Store data or methods without manual mapping to components | Yes | Yes | No |
redux counter example
mobx counter example
concent counter example
State modification | concent | mbox | redux |
---|---|---|---|
Based on the principle of immutability | Yes | No | Yes |
Shortest link | Yes | Yes | No |
ui source traceable | Yes | No | No |
Without this | Yes | No | Yes |
Atomic split & merge commit | Yes(based on lazy) | Yes(based on transaction) | No |
Dependent collection | concent | mbox | redux |
---|---|---|---|
Support runtime collection of dependencies | Yes | Yes | No |
Precise rendering | Yes | Yes | No |
Without this | Yes | No | No |
Only one API is needed | Yes | No | No |
Derived data | concent | mbox | redux(reselect) |
---|---|---|---|
Automatically maintain dependencies between calculation results | Yes | Yes | No |
Collect dependencies when triggering to read calculation results | Yes | Yes | No |
Calculation function without this | Yes | No | Yes |
redux computed example
mobx computed example
concent computed example
todo-mvc combat
redux todo-mvc
mobx todo-mvc
concent todo-mvc
round 1 - Code style first experience
The counter has been promoted to the stage countless times as a good guy in the demo world. This time we are no exception. Come to a counter to experience how the 3 framework development routines are( they are created using create-react-app
). Organize the code in a multi-module way, and strive to be close to the code scenario of the real environment.
redux(action、reducer)
Through models
, the function is divided into different reducers by module, the directory structure is as follows
|____models # business models
| |____index.js # Exposed store
| |____counter # Counter module related actions and reducers
| | |____action.js
| | |____reducer.js
| |____ ... # Other modules
|____CounterCls # Class component
|____CounterFn # Function component
|____index.js # Application entry file
Here only the code is organized with the original template of redux. In the actual situation, many developers may choose
rematch
,dva
and other frameworks based on redux for secondary packaging and improved writing, but it does not prevent us from understanding counter examples.
Construct counter's action
// code in models/counter/action
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const increase = number => {
return { type: INCREMENT, payload: number };
};
export const decrease = number => {
return { type: DECREMENT, payload: number };
};
Construct counter's reducer
// code in models/counter/reducer
import { INCREMENT, DECREMENT } from "./action";
export default (state = { count: 0 }, action) => {
const { type, payload } = action;
switch (type) {
case INCREMENT:
return { ...state, count: state.count + payload };
case DECREMENT:
return { ...state, count: state.count - payload };
default:
return state;
}
};
Combine reducer
to constructstore
and inject into root component
mport { createStore, combineReducers } from "redux";
import countReducer from "./models/counter/reducer";
const store = createStore(combineReducers({counter:countReducer}));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Use connect to connect ui with store
import React from "react";
import { connect } from "react-redux";
import { increase, decrease } from "./redux/action";
@connect(
state => ({ count: state.counter.count }),// mapStateToProps
dispatch => ({// mapDispatchToProps
increase: () => dispatch(increase(1)),
decrease: () => dispatch(decrease(1))
}),
)
class Counter extends React.Component {
render() {
const { count, increase, decrease } = this.props;
return (
<div>
<h1>Count : {count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</div>
);
}
}
export default Counter;
The above example wrote a class component, and for the now hot hook
,redux v7
also released the corresponding api useSelector
,useDispatch
import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as counterAction from "models/counter/action";
const Counter = () => {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
const increase = () => dispatch(counterAction.increase(1));
const decrease = () => dispatch(counterAction.decrease(1));
return (
<>
<h1>Fn Count : {count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</>
);
};
export default Counter;
Render these two counters, View redux example
function App() {
return (
<div className="App">
<CounterCls/>
<CounterFn/>
</div>
);
}
mobx(store, inject)
When there are multiple stores in the application (here we can understand a store as a reducer block in redux, which aggregates data, derived data, and modify behaviors), mobx stores have multiple ways to obtain them, for example, directly where needed Introduced on member variables
import someStore from 'models/foo';// Is an instantiated store instance
@observer
class Comp extends React.Component{
foo = someStore;
render(){
this.foo.callFn();//call method
const text = this.foo.text;//read data
}
}
We are here to do in accordance with accepted best practices, that is, assemble all stores into a root store and hang it on the Provider, wrap the Provider with the entire application root component, and mark the inject
decorator where it is used. Our The directory structure is ultimately as follows, no difference from the redux
version
|____models # business models
| |____index.js # Exposed store
| |____counter # counter module related store
| | |____store.js
| |____ ... # Other modules
|____CounterCls # Class component
|____CounterFn # Function component
|____index.js # Application entry file
Construct counter's store
import { observable, action, computed } from "mobx";
class CounterStore {
@observable
count = 0;
@action.bound
increment() {
this.count++;
}
@action.bound
decrement() {
this.count--;
}
}
export default new CounterStore();
Merge all store
intoroot store
and inject into root component
// code in models/index.js
import counter from './counter';
import login from './login';
export default {
counter,
login,
}
// code in index.js
import React, { Component } from "react";
import { render } from "react-dom";
import { Provider } from "mobx-react";
import store from "./models";
import CounterCls from "./CounterCls";
import CounterFn from "./CounterFn";
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Create a class component
import React, { Component } from "react";
import { observer, inject } from "mobx-react";
@inject("store")
@observer
class CounterCls extends Component {
render() {
const counter = this.props.store.counter;
return (
<div>
<div> class Counter {counter.count}</div>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
</div>
);
}
}
export default CounterCls;
Create a function component
import React from "react";
import { useObserver, observer } from "mobx-react";
import store from "./models";
const CounterFn = () => {
const { counter } = store;
return useObserver(() => (
<div>
<div> class Counter {counter.count}</div>
<button onClick={counter.increment}>++</button>
<button onClick={counter.decrement}>--</button>
</div>
));
};
export default CounterFn;
Render these two counters, View mobx example
function App() {
return (
<div className="App">
<CounterCls/>
<CounterFn/>
</div>
);
}
concent(reducer, register)
Just like redux, Concent also have a global single root state RootStore
, in this root state the first layer of key is used as a module namespace, a module of concent must be configured withstate
, the remaining reducer
,computed
,Watch
, and init
are optional and can be configured as needed. If all the store modules are written to one place, the simplest version ofconcent
is as follows
import { run, setState, getState, dispatch } from 'concent';
run({
counter:{// 配置counter模块
state: { count: 0 }, // [Required] Define the initial state, which can also be written as a function () => ({count: 0})
// reducer: { ...}, // [Optional] How to modify the status
// computed: { ...}, // [Optional] Calculation function
// watch: { ...}, // [Optional] Observation function
// init: { ...}, // [Optional] asynchronous initialization state function
}
});
const count = getState('counter').count;// count is: 0
// count is: 1,如果有组件属于该模块则会被触发重渲染
setState('counter', {count:count + 1});
// If counter.reducer is defined, the changeCount method is defined
// dispatch('counter/changeCount')
After starting concent
to load the store, you can register it in any other component or function component to belong to a specified module or connect multiple modules
import { useConcent, register } from 'concent';
function FnComp(){
const { state, setState, dispatch } = useConcent('counter');
// return ui ...
}
@register('counter')
class ClassComp extends React.Component(){
render(){
const { state, setState, dispatch } = this.ctx;
// return ui ...
}
}
However, it is recommended to put the module definition options in each file to achieve the effect of clear responsibilities and separation of concerns, so for counters, the directory structure is as follows
|____models # business models
| |____index.js # Configure store modules
| |____counter # Counter module related
| | |____state.js # State
| | |____reducer.js # Reducer function
| | |____index.js # Exposing the counter module
| |____ ... # Other modules
|____CounterCls # Class component
|____CounterFn # Function component
|____index.js # Application entry file
|____runConcent.js # Start concent
Construct the counter's state
andreducer
// code in models/counter/state.js
export default {
count: 0,
}
// code in models/counter/reducer.js
export function increase(count, moduleState) {
return { count: moduleState.count + count };
}
export function decrease(count, moduleState) {
return { count: moduleState.count - count };
}
Two ways to configure store
- Configured in the run function
import counter from 'models/counter';
run({counter});
- Configured through the
configure
interface, therun
interface is only responsible for starting concent
// code in runConcent.js
import { run } from 'concent';
run();
// code in models/counter/index.js
import state from './state';
import * as reducer from './reducer';
import { configure } from 'concent';
configure('counter', {state, reducer});// 配置counter模块
Create a function component
import * as React from "react";
import { useConcent } from "concent";
const Counter = () => {
const { state, dispatch } = useConcent("counter");
const increase = () => dispatch("increase", 1);
const decrease = () => dispatch("decrease", 1);
return (
<>
<h1>Fn Count : {state.count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</>
);
};
export default Counter;
Function components are written according to the traditional "hook" style, that is, every time the "hook" function is rendered and executed, the basic interface returned by the "hook" function is used to define an action function that meets the following conditions: the current Business needs.
However, since Concent provides the setup
interface, we can use its ability to execute only once before the initial rendering, and place these action functions inside thesetup
as static functions to avoid repeated definitions, so a better function component should be
import * as React from "react";
import { useConcent } from "concent";
export const setup = ctx => {
return {
// better than ctx.dispatch('increase', 1);
increase: () => ctx.moduleReducer.increase(1),
decrease: () => ctx.moduleReducer.decrease(1)
};
};
const CounterBetter = () => {
const { state, settings } = useConcent({ module: "counter", setup });
const { increase, decrease } = settings;
// return ui...
};
export default CounterBetter;
Create a class component and reuse the logic in setup
import React from "react";
import { register } from "concent";
import { setup } from './CounterFn';
@register({module:'counter', setup})
class Counter extends React.Component {
render() {
// this.state has the same effect as this.ctx.state
const { state, settings } = this.ctx;
// return ui...
}
}
export default Counter;
render these two counters, View example of concentration
function App() {
return (
<div className="App">
<CounterCls />
<CounterFn />
</div>
);
}
Review and summary
This round shows the different code organization and structure when the three framework pairs define multi-module state
-
redux
wraps the root component throughcombineReducers
withProvider
, and also receives handwritingmapStateToProps
andmapActionToProps
to assist the component to obtain data and methods of the store -
mobx
by combining multiplesubStore
into astore
object and collaborating withProvider
to wrap the root component, store data and methods can be obtained directly -
concent
is configured through therun
interface or the separate configuration of theconfigure
interface, the data and methods of the store can be obtained directly
store configuration | concent | mobx | redux |
---|---|---|---|
Support separation | Yes | Yes | No |
No root Provider & use without explicit import | Yes | No | No |
reducer without this
|
Yes | No | Yes |
Store data or methods without manual mapping to components | Yes | Yes | No |
round 2 - State modification
The three frames have different styles of state modification.
In redux
, the state modification path is strictly limited, so all actions to modify the state must dispatch an action, and then hit the correspondingreducer
to synthesize a new state.
mobx
has the responsive ability, you can directly modify it, but it also brings the annoyance that the data modification path cannot be traced back, resulting inmobx-state-tree
to support the modification of data modification.
The modification of concent
completely follows thesetState
modification entry style of react
. On this basis, it further encapsulates thedispatch
, invoke
, andsync
series APIs, and no matter which API is called Both can not only trace the complete link of data modification, but also include the source of triggering data modification.
redux(dispatch)
Synchronous action
export const changeFirstName = firstName => {
return {
type: CHANGE_FIRST_NAME,
payload: firstName
};
};
Asynchronous actions, completed with the help of redux-thunk
// code in models/index.js, configure thunk middleware
import thunk from "redux-thunk";
import { createStore, combineReducers, applyMiddleware } from "redux";
const store = createStore(combineReducers({...}), applyMiddleware(thunk));
// code in models/login/action.js
export const CHANGE_FIRST_NAME = "CHANGE_FIRST_NAME";
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
// Tool function, assist in writing asynchronous actions
const asyncAction = asyncFn => {
return dispatch => {
asyncFn(dispatch).then(ret => {
if(ret){
const [type, payload] = ret;
dispatch({ type, payload });
}
}).catch(err=>alert(err));
};
};
export const asyncChangeFirstName = firstName => {
return asyncAction(async (dispatch) => {//can be used for intermediate process multiple dispatch
await delay();
return [CHANGE_FIRST_NAME, firstName];
});
};
mobx version (this.XXX)
Synchronous action and asynchronous action
import { observable, action, computed } from "mobx";
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
class LoginStore {
@observable firstName = "";
@observable lastName = "";
@action.bound
changeFirstName(firstName) {
this.firstName = firstName;
}
@action.bound
async asyncChangeFirstName(firstName) {
await delay();
this.firstName = firstName;
}
@action.bound
changeLastName(lastName) {
this.lastName = lastName;
}
}
export default new LoginStore();
Direct modification
const LoginFn = () => {
const { login } = store;
const changeFirstName = e => login.firstName = e.target.value;
// ...
}
Modify by action
const LoginFn = () => {
const { login } = store;
const const changeFirstName = e => login.changeFirstName(e.target.value);
// ...
}
concent(dispatch,setState,invoke,sync)
There is no longer any distinction between action
andreducer
in concent. The ui can directly call the reducer
method. At the same time, thereducer
method can be synchronous or asynchronous. It supports arbitrary combinations and lazy calls with each other, which greatly reduces the developer ’s mind. burden.
Synchronous reducer
and asynchronousreducer
// code in models/login/reducer.js
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
export function changeFirstName(firstName) {
return { firstName };
}
export async function asyncChangeFirstName(firstName) {
await delay();
return { firstName };
}
export function changeLastName(lastName) {
return { lastName };
}
The reducers can be combined arbitrarily. The methods in the same module can be directly called based on the method reference. The reducer function is not mandatory to return a new fragment state. It is also possible to combine other reducers.
// reducerFn(payload:any, moduleState:{}, actionCtx:IActionCtx)
// When lazy calls this function, any one of the functions goes wrong, and all the states generated by the intermediate process will not be submitted to the store
export async changeFirstNameAndLastName([firstName, lastName], m, ac){
await ac.dispatch(changeFirstName, firstName);
await ac.dispatch(changeFirstName, lastName);
// return {someNew:'xxx'};//可选择此reducer也返回新的片断状态
}
// View
function UI(){
const ctx useConcent('login');
// Trigger two renderings
const normalCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last']);
// Trigger a render
const lazyCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last'], {lazy:true});
return (
<>
<button onClick={handleClick}> normalCall </button>
<button onClick={handleClick}> lazyCall </button>
</>
)
}
Non-lazy calling process
Lazy calling process
Of course, except for reducer
, the other three methods can be matched arbitrarily, and have the same synchronization state asreducer
to other instances that belong to the same module and depend on a certain state
- setState
function FnUI(){
const {setState} = useConcent('login');
const changeName = e=> setState({firstName:e.target.name});
// ... return ui
}
@register('login')
class ClsUI extends React.Component{
changeName = e=> this.setState({firstName:e.target.name})
render(){...}
}
- invoke
function _changeName(firstName){
return {firstName};
}
function FnUI(){
const {invoke} = useConcent('login');
const changeName = e=> invoke(_changeName, e.target.name);
// ... return ui
}
@register('login')
class ClsUI extends React.Component{
changeName = e=> this.ctx.invoke(_changeName, e.target.name)
render(){...}
}
- sync
For more about sync, check the App2-1-sync.js file
function FnUI(){
const {sync, state} = useConcent('login');
return <input value={state.firstName} onChange={sync('firstName')} />
}
@register('login')
class ClsUI extends React.Component{
changeName = e=> this.ctx.invoke(_changeName, e.target.name)
render(){
return <input value={this.state.firstName} onChange={this.ctx.sync('firstName')} />
}
}
Remember that we mentioned this sentence to the concent before round 2 started to compare: ** Can we not only trace the complete link of data modification, but also include the source of triggering data modification **, what does it mean, because every concent component The ctx
all have a unique idccUniqueKey
to identify the current component instance, it is automatically generated according to {className} _ {randomTag} _ {seq}
, that is, the class name (not provided is the component type $$ CClass
, $$CCHook
) plus random tags and self-increasing serial numbers, if you want to track and modify the source ui, you can manually maintain the tag
andccClassKey
, and then cooperate with concent- plugin-redux-devtool will accomplish our goal.
function FnUI(){
const {sync, state, ccUniqueKey} = useConcent({module:'login', tag:'xxx'}, 'FnUI');
// tag can be added or not added,
// Without tag, ccUniqueKey looks like: FnUI_xtst4x_1
// Tag added, ccUniqueKey looks like: FnUI_xxx_1
}
@register({module:'login', tag:'yyy'}, 'ClsUI')
class ClsUI extends React.Component{...}
After accessing concent-plugin-redux-devtool
, you can see that any action modification Action will contain a fieldccUniqueKey
.
Review and summary
In this round, we made a comprehensive comparison of data modification methods, so that developers can understand from the perspective of concent
, all aspects of the developer's coding experience to make great efforts.
Regarding the state update method, compared with redux
, when all our action flows are minimized, there is no action-> reducer such a link, and it does not matter whether the stored function or the side effect function is distinguished (rematch
, dva
etc. Concept), it is more convenient and clearer to give these concepts to the js
syntax itself. If you need pure functions, just writeexport function
, and if you need side-effect functions, write export async function
.
In contrast to mobx
, everything is a basic function that can be disassembled in any combination. Withoutthis
, it is completely oriented to FP, giving an input expected
output`. This way is also more friendly to the test container.
State modification | concent | mbox | redux |
---|---|---|---|
Based on the principle of immutability | Yes | No | Yes |
Shortest link | Yes | Yes | No |
ui source traceable | Yes | No | No |
Without this | Yes | No | Yes |
Atomic split & merge commit | Yes(based on lazy) | Yes(based on transaction) | No |
round 3 - Dependency collection
This round is a very heavy part. Dependency collection allows ui rendering to keep the minimum range of updates, that is, accurate updates, so vue
will outperform react
in certain tests. When we plug in the dependent collection After the wings, see what more interesting things will happen.
Before we start talking about dependency collection
, let ’s review the original rendering mechanism of react
. When a certain component changes state, if its custom component is not manually maintained by shouldComponentUpdate
, it will always start from All rendering is done up and down, and the cconnect
interface of redux
takes over the shouldComponentUpdate
behavior. When an action triggers an action modification, all connected components will update the state and current status of the mapStateToProps
from the previous moment. The state obtained by mapStateToProps
is shallowly compared to decide whether to refresh the wrapped subcomponents.
In the era of hooks, React.memo
is provided to prevent users from blocking such" plant-based "updates, but users need to pass as much as possible primitive data or unchanged references to props
, otherwise React.memo
The shallow comparison will return false.
But one problem with redux
is that if a state is no longer in use at a certain moment in the view, it should not be rendered but rendered, and mobx
is carried based on the minimal subscription to the data obtained by the ui at runtime The concept of subsets elegantly solves this problem, but concent
is a step closer to hiding the collection behavior more elegantly. Users do n’t need to know the relevant terms and concepts such as observable. Depends on the value, and the next rendering should remove the dependency on the value behavior of a certain stateKey
, this vue is doing very well, in order to make
react have a more elegant and comprehensive dependency collection mechanism ,
Concent` also made a lot of efforts.
redux version (not supported)
Solving dependency collection is not the original intention of the birth of redux
, here we can only silently invite it to the candidate area to participate in the next round of contests.
mobx version (observable, computed, useObserver)
Use decorators or decorate
functions to mark attributes to be observed or calculated
import { observable, action, computed } from "mobx";
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
class LoginStore {
@observable firstName = "";
@observable lastName = "";
@computed
get fullName(){
return `${this.firstName}_${this.lastName}`
}
@computed
get nickName(){
return `${this.firstName}>>nicknick`
}
@computed
get anotherNickName(){
return `${this.nickName}_another`
}
}
export default new LoginStore();
When using the observation status or settlement result in ui, there is a dependency
- Depends only on the calculation result, component-like writing
@inject("store")
@observer
class LoginCls extends Component {
state = {show:true};
toggle = ()=> this.setState({show:!this.state.show})
render() {
const login = this.props.store.login;
return (
<>
<h1>Cls Small Comp</h1>
<button onClick={this.toggle}>toggle</button>
{this.state.show ? <div> fullName:{login.fullName}</div>: ""}
</>
)
}
}
- Depends only on the calculation result, function component writing
import { useObserver } from "mobx-react";
// When show is true, the current component reads fullName,
// fullName is calculated from firstName and lastName
// so its dependence is firstName, lastName
// when show is false, the current component has no dependencies
export const LoginFnSmall = React.memo((props) => {
const [show, setShow] = React.useState(true);
const toggle = () => setShow(!show);
const { login } = store;
return useObserver(() => {
return (
<>
<h1>Fn Small Comp</h1>
<button onClick={toggle}>toggle</button>
{show ? <div> fullName:{login.fullName}</div>: ""}
</>
)
});
});
There is no difference between relying on state and relying on calculation results, because the relevant results from this.props.login
at runtime produce ui's dependence on data.
concent(state,moduleComputed)
No decorator is needed to mark the observation properties and calculation results, just ordinary json
objects and functions, which are automatically converted intoProxy
objects at runtime.
计算结果依赖
// code in models/login/computed.js
// n: newState, o: oldState, f: fnCtx
// The dependency of fullName is firstName lastName
export function fullName(n, o, f){
return `${n.firstName}_${n.lastName}`;
}
// The dependency of nickName is firstName
export function nickName(n, o, f){
return `${n.firstName}>>nicknick`
}
// anotherNickName makes a second calculation based on the cached result of nickName,
// and the dependency of nickName is firstName
// So the dependency of anotherNickName is firstName,
// please note that this function needs to be placed under nickName
export function anotherNickName(n, o, f){
return `${f.cuVal.nickName}_another`;
}
- Depends only on the calculation result, component-like writing
@register({ module: "login" })
class _LoginClsSmall extends React.Component {
state = {show:true};
render() {
const { state, moduleComputed: mcu, syncBool } = this.ctx;
// When show is true, the instance's dependency is firstName + lastName
// When false, there is no dependency
return (
<>
<h1>Fn Small Comp</h1>
<button onClick={syncBool("show")}>toggle</button>
{state.show ? <div> fullName:{mcu.fullName}</div> : ""}
</>
);
}
}
- Depends only on the calculation result, function component writing
export const LoginFnSmall = React.memo(props => {
const { state, moduleComputed: mcu, syncBool } = useConcent({
module: "login",
state: { show: true }
});
return (
<>
<h1>Fn Small Comp</h1>
<button onClick={syncBool("show")}>toggle</button>
{state.show ? <div> fullName:{mcu.fullName}</div> : ""}
</>
);
});
As with mobx
, there is no difference between having a dependency on the state and a calculation result. Obtaining the relevant results from ctx.state at runtime creates the dependency of ui on the data. Every time you renderconcent
The latest dependencies of the current instance are collected dynamically, and the disappeared dependencies are removed during the instance didUpdate
phase.
- Life cycle dependence
The concent
architecture unifies the life cycle functions of class components and function components, so when a state is changed, the life cycle functions that depend on it will be triggered, and support the logic shared by classes and functions
export const setupSm = ctx=>{
// When the firstName changes, the component will be triggered after rendering
ctx.effect(()=>{
console.log('fisrtName changed', ctx.state.fisrtName);
}, ['firstName'])
}
// Used in class components
export const LoginFnSmall = React.memo(props => {
console.log('Fn Comp ' + props.tag);
const { state, moduleComputed: mcu, sync } = useConcent({
module: "login",setup: setupSm, state: { show: true }
});
//...
}
// Used in function components
@register({ module: "login", setup:setupSm })
class _LoginClsSmall extends React.Component {...}
Review and summary
In the round of dependency collection, the dependent collection form of concent
and the component expression form are very different frommobx
. There is no other extra API involved in the entire dependency collection process, and mbox
needs to usecomputed
Modify the getter field. In the function component, you need to use the useObserver
package status to return to the UI.Concent
pays more attention to all functions. The keyword this
is eliminated in the process of organizing the calculation code. ThefnCtx
function context is used to pass the Calculate the results, while explicitly distinguishing the container objects of state
andcomputed
.
Dependent collection | concent | mbox | redux |
---|---|---|---|
Support runtime collection of dependencies | Yes | Yes | No |
Precise rendering | Yes | Yes | No |
Without this | Yes | No | No |
Only one API is needed | Yes | No | No |
round 4 - Derived data
Remember the slogan of mobx
? Any content that can be derived from the application state should be derived, revealing a problem that does exist and we cannot escape. Most application states are accompanied by a calculation process before being used by ui, and the calculation result is called Derived data.
We all know that this concept has been built into vue
, which exposes an optioncomputed
to process the calculation process and cache derived data. React
does not have this concept, andredux
does not provide this capability. However, the open middleware mechanism of redux
allows the community to find an entry point to support this capability, so here the calculation we have mentioned forredux
has become the de facto popular standard library reslect
.
Both mobx
andconcent
have their own calculation support. We have demonstrated the derived data codes of mobx
andconcent
in the ** Dependency Collection ** round above, so this round only writes derivatives for redux
Sample data
redux(reselect)
Redux recently released the v7
version, which exposes two APIs,useDispatch
and useSelector
. The usage is completely equivalent to the previousmapStateToState
and mapDispatchToProps
. In our example, we will use both class components and function components. come out.
定义selector
import { createSelector } from "reselect";
// getter, only used to get the value, does not participate in the calculation
const getFirstName = state => state.login.firstName;
const getLastName = state => state.login.lastName;
// selector,Equivalent to computed, manually import the calculation dependencies
export const selectFullName = createSelector(
[getFirstName, getLastName],
(firstName, lastName) => `${firstName}_${lastName}`
);
export const selectNickName = createSelector(
[getFirstName],
(firstName) => `${firstName}>>nicknick`
);
export const selectAnotherNickName = createSelector(
[selectNickName],
(nickname) => `${nickname}_another`
);
Class component gets selector
import React from "react";
import { connect } from "react-redux";
import * as loginAction from "models/login/action";
import {
selectFullName,
selectNickName,
selectAnotherNickName
} from "models/login/selector";
@connect(
state => ({
firstName: state.login.firstName,
lastName: state.login.lastName,
fullName: selectFullName(state),
nickName: selectNickName(state),
anotherNickName: selectAnotherNickName(state),
}), // mapStateToProps
dispatch => ({
// mapDispatchToProps
changeFirstName: e =>
dispatch(loginAction.changeFirstName(e.target.value)),
asyncChangeFirstName: e =>
dispatch(loginAction.asyncChangeFirstName(e.target.value)),
changeLastName: e => dispatch(loginAction.changeLastName(e.target.value))
})
)
class Counter extends React.Component {
render() {
const {
firstName,
lastName,
fullName,
nickName,
anotherNickName,
changeFirstName,
asyncChangeFirstName,
changeLastName
} = this.props;
return 'ui ...'
}
}
export default Counter;
Function component gets selector
import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as loginAction from "models/login/action";
import {
selectFullName,
selectNickName,
selectAnotherNickName
} from "models/login/selector";
const Counter = () => {
const { firstName, lastName } = useSelector(state => state.login);
const fullName = useSelector(selectFullName);
const nickName = useSelector(selectNickName);
const anotherNickName = useSelector(selectAnotherNickName);
const dispatch = useDispatch();
const changeFirstName = (e) => dispatch(loginAction.changeFirstName(e.target.value));
const asyncChangeFirstName = (e) => dispatch(loginAction.asyncChangeFirstName(e.target.value));
const changeLastName = (e) => dispatch(loginAction.changeLastName(e.target.value));
return 'ui...'
);
};
export default Counter;
Online example of redux derivative data
mobx (computed decorator)
See the example code above dependent on collection, no longer restated here.
concent (directly obtained by moduleComputed)
See the example code above dependent on collection, no longer restated here.
Review and summary
Compared to mobx
, which can be obtained directly fromthis.pops.someStore
, concent
can be obtained directly fromctx.moduleComputed
. There is an additional process of manually maintaining calculation dependencies or mapping selection results. The way that developers are more willing to use this result is clear at a glance.
Derived data | concent | mbox | redux(reselect) |
---|---|---|---|
Automatically maintain dependencies between calculation results | Yes | Yes | No |
Collect dependencies when triggering to read calculation results | Yes | Yes | No |
Calculation function without this | Yes | No | Yes |
round 5 - Combat TodoMvc
The four rounds above combined a live code example, summarizing the characteristics and coding styles of the three frameworks. I believe that readers expect to have a code example that is closer to the production environment to see the difference. Then let us finally take "TodoMvc" comes to an end to this feature competition. I hope you can learn more about and experience concent
and start the react programming journey of immutable & dependent collection.
redux-todo-mvc
action related
reducer related
computed related
mobx-todo-mvc
action related
computed related
concent-todo-mvc
reducer related
computed related
## end
Finally, let us end this article with a minimal version of the concent application. Will you choose concent as your react development weapon in the future?
import React from "react";
import "./styles.css";
import { run, useConcent, defWatch } from 'concent';
run({
login:{
state:{
name:'c2',
addr:'bj',
info:{
sex: '1',
grade: '19',
}
},
reducer:{
selectSex(sex, moduleState){
const info = moduleState.info;
info.sex = sex;
return {info};
}
},
computed: {
funnyName(newState){
// The dependency corresponding to the collected funnyName is name
return `${newState.name}_${Date.now()}`
},
otherFunnyName(newState, oldState, fnCtx){
// Get the calculation result of funnyName and newState.addr as input to calculate again
// So the dependency corresponding to otherFunnyName collected here is name addr
return `${fnCtx.cuVal.funnyName}_${newState.addr}`
}
},
watch:{
// watchKey name and stateKey have the same name, and watch name changes by default
name(newState, oldState){
console.log(`name changed from ${newState.name} to ${oldState.name}`);
},
// The values of addr and info are read from newState,
// the current watch function depends on addr and info,
// when any one of them changes, this watch function will be triggered
addrOrInfoChanged: defWatch((newState, oldState, fnCtx)=>{
const {addr, info} = newState;
if(fnCtx.isFirstCall)return;// Just to collect dependencies, do not execute logic
console.log(`addr is${addr}, info is${JSON.stringify(info)}`);
}, {immediate:true})
}
}
})
function UI(){
console.log('UI with state value');
const {state, sync, dispatch} = useConcent('login');
return (
<div>
name:<input value={state.name} onChange={sync('name')} />
addr:<input value={state.addr} onChange={sync('addr')} />
<br />
info.sex:<input value={state.info.sex} onChange={sync('info.sex')} />
info.grade:<input value={state.info.grade} onChange={sync('info.grade')} />
<br />
<select value={state.info.sex} onChange={(e)=>dispatch('selectSex', e.target.value)}>
<option value="male">male</option>
<option value="female">female</option>
</select>
</div>
);
}
function UI2(){
console.log('UI2 with comptued value');
const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
return (
<div>
{/*
When show is true, the dependency of the current component
is the dependency name corresponding to funnyName
*/}
{state.show? <span>dep is name: {moduleComputed.funnyName}</span> : 'UI2 no deps now'}
<br/><button onClick={syncBool('show')}>toggle show</button>
</div>
);
}
function UI3(){
console.log('UI3 with comptued value');
const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
return (
<div>
{/*
When show is true, the dependency of the current component
is the dependency corresponding to funnyName name addr
*/}
{state.show? <span>dep is name,addr: {moduleComputed.otherFunnyName}</span> : 'UI3 no deps now'}
<br/><button onClick={syncBool('show')}>toggle show</button>
</div>
);
}
export default function App() {
return (
<div className="App">
<h3>try click toggle btn and open console to see render log</h3>
<UI />
<UI />
<UI2 />
<UI3 />
</div>
);
}
❤ star me if you like concent ^_^
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.
Posted on April 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.