preface

Redux and MOBx are not used in this article. Redux and MOBx are not used in this article. If you need to learn how to use redux and MOBx, please refer to the redux and MOBx documentation

A story,

1. Redux and React?

React uses data to drive views, while Redux emphasizes one-to-one correspondence between views and states, so the redux-based state management model fits well with React. Instead of talking about component communication, let’s take a simple counting component as an example

//React version: A button click in the Component triggers a state(count) change (onChange) that drives a view (UI) changefunction Count() {
  const [ count, setCount ] = useState(0)
  return (
    <div className="App">
      <h1>count: {count}</h1>
      <button onClick={()=>{
        setCount(c => c+1)
      }}>+1</button>
      <br />
         <button onClick={()=>{
        setCount(c => c-1) }}>-1</button> </div> ); } // Redux version: // React-redux is a react-redux system that uses useReducer to simulate redux in react. // like the initialState of the global store, as the root reducer parameter const initialState = {count: 0}; / / root reducerfunction reducer(state, action) {
  const { count } = state;
  if (action.type === "increment") {
    return { ...state, count: count + 1 };
  } else if (action.type === "decrement") {
    return{... state, count: count - 1 }; }}function CountCreateStore const [state, dispatch] = useReducer(reducer state);return (
    <div className="App">
      <h1>count: {state.count}</h1>
      <button
        onClick={() => {
          dispatch({ type: "increment" });
        }}
      >
        +1
      </button>
      <br />
      <button
        onClick={() => {
          dispatch({ type: "decrement" });
        }}
      >
        -1
      </button>
    </div>
  );
}
Copy the code

2. Redux core concepts

In terms of core concepts, my idea is to summarize what it is and then look at the source code with questions (PS: looking at the source code with questions is my favorite operation).

  • Store: a place where state is stored centrally
  • Action: “change”, a normal JS object, is the only way to change the state
  • Reducer: “change” a pure function that changes the corresponding state in the store according to actions
  • Middleware: “strong” Middleware, which can be interpreted as pre-processing Action or enhancing store

const { dispatch, getState, ... restStore } = createStore(reducer,[[preloadedState]],[[enhancer]]);Copy the code

As you can see, Redux creates a Store using the createStore method. We take the following questions and analyze them with the source code

  1. Action is an object. How do I inform reducer to change the state in the Store?

    * The only way to change The data in The store is to call ‘dispatch()’ on it. So we venture to assume that there must be an action like nextState = rootReducer(prevState, Action) in the dispatch method returned by createStore

    function dispatch() {... try { isDispatching =true
          currentState = currentReducer(currentState, action)
        } finally {
          isDispatching = false}... }Copy the code

    If you’ve used Redux, you might be wondering: In practice, it looks like we didn’t call it through store.dispatch(action). How did we get here?

    In practice, actions are usually not directly described by objects, but by creating an actionCreator. (Personally, this is more consistent with the first-class citizen feature of functions and the strong FP thought in REUDX. On the other hand, functions can handle other complex logic. This makes the reducer simple enough) to generate actions, so the reducer needs to be informed each time through store.dispatch(actionCreator()), but this coupling degree is relatively high and it is not convenient and intuitive to understand. So Redux provided bindActionCreators to avoid an explicit call to Store. dispatch. My understanding is that each function does its job and is combined into a new function (FP sweet). Taking a bold guess, bindActionCreators passed in an object to unbind and then returned a bound object. Looking at the source code, bindActionCreators also allows you to pass in a single function and then return a bound function for you

    // This time the name isbindActionCreator, without s, does the same as store.dispatch(ActionCreator ()) abovefunction bindActionCreator<A extends AnyAction = AnyAction>(
      actionCreator: ActionCreator<A>,
      dispatch: Dispatch
    ) {
      return function(this: any, ... args: any[]) {returnDispatch (actionCreator. Apply (this, args))}} // accept different parameters and do different processingfunction bindActionCreators (ActionCreators: ActionCreator < any > | ActionCreatorsMapObject, dispatch: dispatch) {/ / incoming objectif (typeof actionCreators === 'function') {
        return bindActionCreator(actionCreators, Dispatch)} // Handle the error parametersif(typeof actionCreators ! = ='object' || actionCreators === null) {
        throw new Error(
          `bindActionCreators expected an object or a function, instead received ${
            actionCreators === null ? 'null' : typeof actionCreators
          }. ` +
            `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ')} // Incoming object const boundActionCreators: ActionCreatorsMapObject = {}for (const key in actionCreators) {
        const actionCreator = actionCreators[key]
        if (typeof actionCreator === 'function') {
          boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
        }
      }
      return boundActionCreators
    }
    Copy the code
  2. Why return a getState function to get the state in the store, isn’t it good to return state directly?

    In fact, look at the source getState method can already answer this question. In fact, it is quite understandable that isDispatching is added here, because if you have sent an action to the reducer through dispatch, then the state you got at this moment is meaningless, because he may change in the next second

    function getState(): S {
        if (isDispatching) {
          throw new Error(
            'You may not call store.getState() while the reducer is executing. ' +
              'The reducer has already received the state as an argument. ' +
              'Pass it down from the top reducer instead of reading it from the store.')}return currentState as S
      }
    
    Copy the code
  3. There is only one REDUCER when creating a store. Do we write all reducer together?

    When creating a store, there can only be one reducer, but it is obviously unreasonable to write all unrelated actions in a reducer. Therefore, Redux provided us with a combineReducer method to merge all reducer, which perfectly solved the problem. CombineReducer is a more interesting operation, here is not detailed, briefly talk about the results of the implementation, interested in your own source ~

    // It is not difficult to understand that combineReducer must eventually return a function rootReducer, RootReducer accepts state and action const rootReducer = combineReducer({key1: Reducer1, key2: Reducer2,... } cosnt rootState = {key1: Reducer1 State, KEY2: Reducer2 State,... } // Similarly, finalReducers const finalReducers = {key1: reducer1, key2: Reducer2,... } // If the user sends an action, we can locate the reducer and state const nextState = finalReducers[key](rootState[key], action)Copy the code
  4. What the hell is enhancer?

    Enhancer can only be undefined or function. If enhancer is a function, CreateStore (createStore)(reducer, preloadedState) ¶ ApplyMiddleware (Reducer, applyMiddleware([thunk]], [logger]]) ), so we probably know what enhancer is

    * @param enhancer The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities  such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. const enhancer = applyMiddleware(... middlewares) cosnt applyMiddleware = createStore => (reducer, ... rest) => { const store = createStore(reducer, ... rest); Const middlewareAPI: middlewareAPI = {getState: store.getState, dispatch: (action,... args) => dispatch(action, ... Args)} // Take the middlewareAPI as a parameter to traverse all middleware and get a chain of functions ([fn1, Fn2, fn3... Middlewares. map(Middleware => middlewareAPI) // const chain => middleware Compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose Const enhanceDispatch = compose<typeof dispatch>() const enhanceDispatch = compose<typeof dispatch>(... chain)(store.dispatch)return{ dispatch: enhanceDispatch, ... store } }Copy the code

3. Redux design idea

When it comes to design ideas, be sure to go back to documentation.

This complexity is difficult to handle as we’re mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke. Both can be great in separation, but together they create a mess. Libraries like React attempt to solve this problem in the view layer by removing both asynchrony and direct DOM manipulation. However, managing the state of your data is left up to you. This is where Redux enters.



Following in the steps of Flux, CQRS, and Event Sourcing, Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.

To explain a few words in general:

Flux: a data management idea that emphasizes one-way data flow and data stored only in store

② CQRS(Command Query Responsibility Segregation) : Literal translation is Command Query Responsibility Segregation, which also reflects the most critical point is read and write Segregation

Event Sourcing: Event Sourcing is a process Sourcing process, which is a process Sourcing process, but not a process Sourcing process. ==> state)

Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can be arranged Happen (Redux tries to make state changes predictable by placing restrictions on how and when updates can be made). Around this central idea, try to analyze his design.

  1. One-way data flow: As with the Flux architecture, one-way data flow is the safest way to be predictable
  2. There is only one store globally: Redux believes that a single store can centralize data and make it easier to manage and forecast, and redux believes that the only responsibility of a store is to store state centrally without too many operations
  3. Read/write separation: This idea is probably at the heart of Redux, where states are read-only and you can pass throughstore.getState()Read, as for write, Redux abandoned the concept of Flux dispatcher and replaced reducer, reducer is sent as parameters in createStore, so as to achieve read and write separation
  4. Reducer must be a pure function: If the function to change the state is too complicated, the state management will not be intuitive and can be predicted. This is why Redux has always emphasized that reducer must be a pure function, but the actual business will not be so smooth, at least there will be request data, Redux will think this is a side effect and not under the reducer management. But the answer is Middleware
  5. Middleware: Redux believed that dispatch(Action) was the only way to notify reducer to change the state. In order to solve the problem of side effects, Redux cleverly introduced the concept of Middleware. The general logic is to allow users to intercept and preprocess actions during the period from dispatch(Action) to reducer state change, which is similar to pipeline splicing. In order to be predictable, Redux expects the pipeline to have single responsibilities and has requirements on the sequence of pipeline splicing. For example, the log printing pipeline must come after the asynchronous processing pipeline, which addresses both side effects and scalability issues

Second, the Mobx

1. Mobx and React?

UI=f(data). Mobx thinks React does f well enough. If it helps developers manage data, it will stand out from the crowd. React and Mobx are a powerful combination. Let’s take a simple counting component as an example

Import {useLocalStore, useObserver} from from {useLocalStore, useObserver} from {useLocalStore, useObserver} from} from {useLocalStore, useObserver} from} from {useLocalStore, useObserver} from"mobx-react-lite"; 

function createCountStore(count) {
  return {
    count, // state
    increment() { // action1
      this.count += 1;
    },
    decrement() { // action2 this.count -= 1; }}; }export default function App() {
  const countStore = useLocalStore(() => createCountStore(0));
  // Derivations--Reactions
  return useObserver(() => {
    return (
      <div className="App">
        <h1>count: {countStore.count}</h1>
        <button onClick={countStore.increment}>+1</button>
        <br />
        <button onClick={countStore.decrement}>-1</button>
      </div>
    );
  });
}
Copy the code

2. Mobx core concepts

  • State: Data that drives the application, such as an Excel spreadsheet with data
  • Derivations: Anything derived from a state without any further interaction is Derivations
    • Computed values: Computed values derived from the current observable state. The Excel formula is derived from Computed values
    • Reactions: A side effect that automatically occurs when your state changes
  • Actions: Any piece of code that can change state, such as entering a new value in an Excel cell

  1. How do I create observable states?

    To manage UI = f(data) data, your state(data) must be under Mobx’s control. Mobx creates observable objects by exposing the Mobx. Observable method. It holds a reference to the createObservable function, which creates various observable types. The observableFactories attribute is a special object, and the observableFactories attribute is hung under the Observable attribute. After simplifying the source code, we find that the corresponding conversion method is called. The specific conversion method will not be described here. DefineProperty (Object defineProperty) defineProperty (Proxy) Object defineProperty (Object defineProperty) Observable, can be combined with the document to eat, the source code effect is better.

const observable = createObservable

// weird trick to keep our typings nicely with our funcs, and still extend the observable function
Object.keys(observableFactories).forEach(name => (observable[name] = observableFactories[name]))

functionCreateObservable (v, arg2, arg3) {// Strategy 1: handle decorator syntaxif (typeof arguments[1] === "string" || typeof arguments[1] === "symbol") {
        returnDeepdecorator.apply (null, arguments as any)} // Strategy 2: Return if the value is already observedif (isObservable(v)) returnV // strategy 3: Object,Array,Map,Set const res = isPlainObject(v)? observable.object(v, arg2, arg3) : Array.isArray(v) ? observable.array(v, arg2) : isES6Map(v) ? observable.map(v, arg2) : isES6Set(v) ? observable.set(v, arg2) : vif(res ! == v)returnRes // strategy 4: For other types, prompt observable. Box fail(process.env.node_env! = ="production"&& `The provided value could not be converted into an observable. If you want just create an observable reference to the  object use'observable.box(value)'`)} / / here found observableFactories basic attribute method have a check whether it is right to use a decorator (incorrectlyUsedAsDecorator) and treatment options of asCreateObservableOptions operation / / here is unified specification o after asCreateObservableOptions processing options const observableFactories = {box (value, options) {return new ObservableValue(value, getEnhancerFromOptions(o), o.name, true, o.equals)
    },
    array(initialValues, options) {
        return createObservableArray(initialValues, getEnhancerFromOptions(o), o.name) as any
    },
    map(initialValues, options) {
        return new ObservableMap(initialValues, getEnhancerFromOptions(o), o.name)
    },
    set(initialValues, options) {
        return new ObservableSet<T>(initialValues, getEnhancerFromOptions(o), o.name)
    },
    object(props, decorators, options) {
        if (o.proxy === false) {
            return extendObservable({}, props, decorators, o)
        } else {
            const defaultDecorator = getDefaultDecoratorFromObjectOptions(o)
            const base = extendObservable({}, undefined, undefined, o)
            const proxy = createDynamicObservableObject(base)
            extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator)
            return proxy
        }
    },
    ref: refDecorator,
    shallow: shallowDecorator,
    deep: deepDecorator,
    struct: refStructDecorator
}
Copy the code
  1. Mobx divides Derivations into Computed values and Reactions. Is there any difference between the two?

    Undoubtedly, they are both derived from state, which means that they can do things when state changes. In my opinion, Mobx tends to design Computed values as a pure function, which means that it eventually returns a new predictable result. Instead of returning a new value, the Design of Reactions is more about achieving a side effect, meaning it’s more about achieving an effect. There is a golden rule in the Chinese documentation that supports the above analysis: If you want to create a value based on the current state, use computed.

// Computed computeValue method computeValue(track) {... // If you need to track and execute tasks, use the trackDerivedFunction to calculate, Otherwise, call this.derivation directly // The Construtor method of the class(ComputedValue) in which this method is adopted has this code: this.derivation = options.get! // Options. get is the last method passed to get. I don't know what it is. I hope someone can answer my questionsif (track) {
        res = trackDerivedFunction(this, this.derivation, this.scope)
    } else{... res = this.derivation.call(this.scope) ... }...returnRes} // Reaction, a variant of autorun, can be roughly thought of as the syntactic sugar of Autorun (() => action(sideEffect)(expression)) // So we turn our attention to Autorun, The key point here is Reaction Class // Since the Mobx migration from Redux is a recent one, the source code is just to clarify the process, so a detailed source code interpretation of this can be seen in the following article: https://segmentfault.com/a/1190000013682735#item-2-4
function autorun( view, opts) {

    const name = (opts && opts.name) || view.name || "Autorun@"+ getNextId() const runSync = ! opts.scheduler && ! opts.delaylet reaction

    if (runSync) {
        reaction = new Reaction(
            name,
            function (this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    } else {
        const scheduler = createSchedulerFromOptions(opts)
        // debounced autorun
        let isScheduled = false

        reaction = new Reaction(
            name,
            () => {
                if(! isScheduled) { isScheduled =true
                    scheduler(() => {
                        isScheduled = false
                        if(! reaction.isDisposed) reaction.track(reactionRunner) }) } }, opts.onError, opts.requiresObservable ) }function reactionRunner() {
        view(reaction)
    }

    reaction.schedule()
    return reaction.getDisposer()
}
Copy the code

3. Mobx design ideas

I have to go back to the documentation to talk about my design ideas. I don’t see any useful points in the documentation, but I find a Mobx author’s blog: Becoming fully Reactive: An in-depth explanation of MobX, WALL crack suggests reading, and the following is my understanding.

Reacting to state changes is always better then acting on state changes.

This sentence is well written, on the one hand, pointing out the theme, on the other hand, rendering the atmosphere of the time (the fault of poetry appreciation again….) D) Reacting to state changes and acting on state changes From the perspective of prepositions, to refers to the response when the state changes, that is, the framework helps you manage the state and respond to the state change; On refers to the effect on the state change, the initiative is in the hands of the user (frame user), issued by the user to determine the action. 31 Reacting to state changes and reacting to state changes

Comparison and application scenarios of Redux and Mobx

Redux vs Mobx

  1. Redux
  • There is only one store globally, clear data flow, and every action triggered will result in traversal of the state tree
  • Read and write separation is realized, and redux has the ability to track back time through reducer storage instead of state(readonly), which makes state mutation and error location more predictable
  • The concept of middleware is introduced to allow users to intercept from dispatch(Action) to reducer state change and preprocess the action. Although it is somewhat obscure (functional programming idea), it has clear responsibilities and can get twice the result with half the effort in flexible use
  • Learning costs are high, and some concepts are difficult to understand, requiring functional programming thinking to better understand
  1. Mobx
  • Storing data in scattered stores tends to be more tailored, and only responds correctly to local state changes
  • There is only one reference to the data, no backtracking capability, but there is also no additional overhead of copying objects
  • State in Mobx is mutable and can be changed directly, and Mobx automatically responds to state changes. This makes state management incredibly easy, but it also makes it difficult to predict and debug
  • And JS coding habits more fit, simple and easy to use

Applicable scenario

Mobx is recommended for applications with less complex data flows, which are simple, convenient and easy to maintain, such as active pages (the data flows of each active page are basically not complex, and there is little connection between activities).

For applications with complex data flow, such as shopping related pages of Taobao, Redux can be considered. Firstly, the business complexity can be reduced through reasonable use of middleware. In addition, Redux is designed to predict state mutations, so applications with high requirements on data prediction should be the best choice. Here’s an article by a Redux author that tells You how redux works: You Might Not Need Redux

This doesn’t mean you can’t use Mobx for complex applications. As long as you have a scientific code structure (especially important for teams because Mobx’s flexibility has to be limited), you can easily handle complex applications with TS.

Here, this long article is basically over, because my ability is limited, if there is any wrong or do not understand the place, welcome to comment area exchange correction!