DEV Community

loading...

Compare redux, mobx and concent in depth to let you have a funny way in developing react app.

fantasticsoul profile image 幻魂 ・23 min read

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 management

  • design concept

    Single data source, use pure functions to modify state

mobx & mobx-react

  • slogan:

    Simple and scalable state management

  • design concept

Anything that can be derived from the application state should be derived

concent

  • slogan:

    Predictable, zero-invasion, progressive, high-performance react development solution

  • design 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

mobx example

concent example


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 with Provider, and also receives handwritingmapStateToProps and mapActionToProps to assist the component to obtain data and methods of the store
  • mobx by combining multiplesubStore into a store 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 the configure 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>
        </>
    )
}

lazyReducer example

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.

View mobx example

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 {...}

View concent example

Read more about ctx.effect

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

View redux-todo-mvc demo

action related

reducer related

computed related

mobx-todo-mvc

View mobx-todo-mvc demo

action related

computed related

concent-todo-mvc

View concent-todo-mvc demo

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 ^_^

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

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

If you have any questions about concent, you can scan the code and add group consultation, will try to answer the questions and help you understand more.

Discussion (0)

Forem Open with the Forem app