The React Demo code used in this article uses the new 16.8 Hooks:

It lets you use state and other React features without having to write a class.

preface

When you start a project, there’s probably only one Root component for all of your code — Root — roll up your sleeves!

As the project progresses, some of the guys break up some of the sub-components, and of course, there will be some data flow between them — not a big enough problem to keep them connected.

Now the project is booming and the business has grown by a factor of N, so we have to dismantle more and more descendant components to realize more and more complex business — hopefully the logic is simple and the data flow is layer by layer down

However, the harsh reality is that the relationship between the paternal and descendant components is often chaotic.

What to do, what to do?

As long as the mind does not slide, the method is more than difficult

  • Solution 1: Sorting out project logic and redesigning component relationships (🤬)
  • Plan 2: Resign and start again with another company (🤔️)

Indeed, in the process of project iteration, it is inevitable to share state between components, which leads to logic interleaving and is difficult to control.

Then we thought, “Could there be a practice that pulls all possible common states, data, and capabilities out of the component, streams them from the top down, and gets them where they need them, instead of prop Drilling?”

A data structure like this emerged:

const store = {
    state: {
        text: 'Goodbye World! '
    },
    setAction (text) {
        this.text = text
    },
    clearAction () {
        this.text = ' '}}Copy the code

There is an external variable store:

  • stateTo store data
  • There’s a bunch of different functionsactionTo control thestateThe change of

Add to that the mandatory constraints: you can only change state by calling an action, and then you have a clear view of what state is doing through action, so what’s the concern with logging, monitoring, rolling back, etc.

In fact, this is the early early form of Flux.

Flux

In 2013, When Facebook unveiled React, Flux followed suit. Facebook believes the two work together to build large JavaScript applications.

As an easy comparison, React is used to replace jQuery, so Flux is used to replace backbone. js, ember.js and other MVC frameworks.

As shown in the figure above, data is always “one-way flow”, and there is no mutual flow of data between adjacent parts, which is also a major feature of Flux.

  • ViewOriginating user’sAction
  • DispatcherAs the dispatch center, receiveAction,StoreUpdate accordingly
  • StoreHandles primary logic and provides listening capabilities that trigger listening events when data is updated
  • ViewListen to theStoreTriggered after the update eventUIupdate

If you’re interested, see what each part means:

Action

The plain javascript object uses type and payload to describe the specific meaning of the action.

In Flux, actions are generally defined: a set of functions that contain the dispatch of action objects.

// actions.js
import AddDispatcher from '@/dispatcher'

export const counterActions = {
    increment (number) {
        const action = {
            type: 'INCREMENT'.payload: number
        }

        AddDispatcher.dispatch(action)
    }
}
Copy the code

Increment is dispatched to Store using counteractions. increment.

Dispatcher

Dispatch the Action to the Store and register a unique instance through the Dispatcher provided by Flux.

The Dispatcher.register method is used to register callbacks for various actions

import { CounterStore } from '@/store'
import AddDispatcher from '@/dispatcher'

AppDispatcher.register(function (action) {
  switch (action.type) {
    case INCREMENT:
      CounterStore.addHandler();
      CounterStore.emitChange();
      break;
    default:
    // no op}});Copy the code

In the code above, AppDispatcher receives INCREMENT and executes a callback to CounterStore.

The Dispatcher should only be used to dispatch actions, there should be no other logic.

Store

The processing center for application state.

The Store handles complex business logic, and the View is responsible for providing the ability to notify the View of updates as data changes.

Because of its on-demand registration, an application can register the ability of multiple stores, update Data Dlow as

The observant will notice that the emitChange method is called in the previous section CounterStore — yes, it is used to notify changes.

import { EventEmitter } from "events"

export const CounterStore = Object.assign({}, EventEmitter.prototype, {
  counter: 0.getCounter: function () {
    return this.counter
  },
  addHandler: function () {
    this.counter++
  },
  emitChange: function () {
    this.emit("change")},addChangeListener: function (callback) {
    this.on("change", callback)
  },
  removeChangeListener: function (callback) {
    this.removeListener("change", callback)
  }
});
Copy the code

In this code, CounterStore obtains the ability to emit and listen for on events by inheriting EventEmitter. Prototype.

View

A view of the data in the Store

The View needs to listen for changes in the View to keep the View up to date, i.e

  • Need to be added in the componentaddChangeListerner
  • Remove the listener when the component is destroyedremoveChangeListener

Let’s take a look at a simple Couter example to better understand how it works in practice.

(Manual segmentation)

Those of you who are serious about it might notice:

  • Click on theresetLater,storeIn thecouterBe updated (noemitChangeSo views are not updated in real time);
  • Interlaced business logic and data processing logic, code organization disorder;

Okay, stop. Let’s look at a new data stream.

Redux

  • The user and theViewinteract
  • throughAction Creatordistributedaction
  • arriveStoreAfter get the currentState, together withReducer
  • ReducerAfter processing, return brand newStatetoStore
  • StoreNotice after UpdateViewTo complete a data update

The basic principle of Flux is “one-way data flow”, and Redux emphasizes on this basis:

  • Single Source of Truth: Keep only one for the entire appStoreIs the data source for all componentsStoreIn the state.
  • Keep the State read-only (State is read-only) : Do not modify the State directlyStoreMust be distributed by aactionObject complete.
  • Changes are made with pure funtions onlyreducer.

If you’re interested, see what each part means:

(ReduxThe source code and its short and elegant, there are friends who want to try to read the source code can start from it)

Store

Application unique data storage center

import { createStore } from 'redux'

const store = createStore(fn)
Copy the code

The code above, using the createStore function provided by Redux, takes another function fn(Reducers, mentioned later) as an argument to generate an application-unique store.

Take a look at the simple implementation of the createStore function

const createStore = (reducer) = > {
  let state
  let listeners = []

  const getState = () = > state

  const dispatch = (action) = > {
    state = reducer(state, action);
    listeners.forEach(listener= > listener())
  }

  const subscribe = (listener) = > {
    listeners.push(listener)
    return () = > {
      listeners = listeners.filter(l= >l ! == listener) } } dispatch({})return { getState, dispatch, subscribe }
}
Copy the code

I look at the source code has a small tip, generally from the export to find, then look at return.

As above, return out three abilities:

  • getState: The only way to get state, which is called a snapshot of the store
  • dispatch: view The only way to dispatch an action
  • subscribe: Registers the listener function (core, later) and returns to unlisten

Notice that at the end of the above code fragment, an empty object was dispatched to generate the initial state. The principle can be explained after learning the reducer writing method.

Of course, createStore can accept more parameters, such as preloadedState (default state), enhancer (Store’s power mushroom), etc., which we will examine later.

Action

The plain javascript object uses type and payload to describe the specific meaning of the action.

In Redux, the type attribute is required and represents the name of the Action. Other attributes can be set freely, as per the specification.

const actions = {
    type: 'ADD_TODO'.payload: 'Learn Redux'
}
Copy the code

AddTodo is an Action Creator that takes different parameters to generate different actions:

function addTodo(text) {
  return {
    type: 'ADD_TODO'.payload: text
  }
}

const action = addTodo('Learn Redux')
Copy the code

reducer

A pure function that updates the store according to action

 (previousState, action) => newState
Copy the code

The above is the reducer function signature, which receives the action from the View and gets the latest state from the store. After processing, a new state update view is returned.

const reducers = (state = defaultState, action) = > {
    const { type, payload } = action
    
    switch (type) {
        case 'ADD_TODO':
            return {
                ...state,
                counter: state.counter + (+payload)
            }
        default:
            return state
    }
}
Copy the code

Above code, the suspense left by createStore can be answered from the default branch.

The result returned by Reducer must be a new state, especially when referring to data types, because React’s judgment on data updates is shallow. If there is the same reference before and after the update, React will ignore this update.

Ideally, the level of state structure may be simple, so what if the descendants of the branches and leaves of the state tree are complex (state.A.B.C)?

const reducers = (state = {}, action) = > {
    const { type, payload } = action
    
    switch(type) {
        case 'ADD':
            return {
                ...state,
                a: {
                    ...state.a,
                    b: {
                        ...state.a.b,
                        c: state.a.b.c.concat(payload)
                    }
                }
            }
        default:
            return state
    }
}
Copy the code

Let’s not discuss the risk of writing above, just look at all the layers of vomit.

In that case, let’s try something else.

As mentioned above, store is unique in Redux, so we only need to ensure that the state returned from reducer is a complete structure. Is that ok?

const reducers = (state = {}, action) = > {
    return {
         A: reducer1(state.A, action),
         B: reducer2(state.B, action),
         C: reducer3(state.C, action)
    }
}
Copy the code

Above, we saved the country by curving, divided the complex data structure, managed different branches of each reducer state tree, and finally merged all reducer into createStore, which is exactly the design idea of combineReducer.

combineReducer

import { combineReducers, createStore } from 'redux'

const reducers = combineReducers({
  A: reducer1,
  B: reducer2,
  C: reducer3
})

const store = createStore(reducers)
Copy the code

Above, the corresponding reducer was executed according to the state key, and the returned result was merged into a large state object.

Take a look at the simple implementation:

const combineReducers = reducers= > (state = {}, action) = > {
    return Object.keys(reducers).reduce((nextState, key) = > {
        nextState[key] = reducers[key](state[key], action)
        return nextState
    }, {})
}
Copy the code

The basic capabilities of Redux have been described above, so let’s take a look at a Demo to deepen the impression.

(Manually split again)

One pain point to note:

  • componentYou have to subscribestore.subscribe``state“, making the code look stupid and not “elegant”.

Flux vs Redux

Well, the fundamentals of Redux are all covered. It is a set of solutions implemented based on the core idea of Flux. From the above analysis, we can feel the difference:

Above, from store and dispatcher two essential difference comparison of the two, I believe that your English must be better than me, I will not translate.

(Don’t ask me why I want mahjong tiles + English arrangement, ask is “Chinese and Western combination”)

Like Flux, Redux is just an idea or specification and has nothing to do with React. Redux supports React, Angular, Ember, jQuery, and even pure JavaScript.

React is a functional, one-way data flow that works well with Redux, so Redux is used for state management.

Of course, Not all projects recommend Redux out of the blue. Dan Abramov mentioned “You Might Not Need Redux” a long time ago. Redux should only be used for react problems, such as:

  • The way users use it is complex
  • Users of different identities have different usage modes (for example, ordinary users and administrators)
  • Multiple users can collaborate/interact heavily with the server, or use websockets
  • The View gets data from multiple sources
  • .

(Manually split again)

Okay, let’s move on to Redux.

Above, we are dealing with synchronous and logically simple Redux usage scenarios. Real business development scenarios are far more complex than this, and various asynchronous tasks are inevitable.

Let’s follow Redux’s Data Flow analysis:

  • View:stateThe visual layer, corresponding to one and one, is not suitable to undertake other functions;
  • Action: Describes an action that can only be performed
  • Reducer: pure function, only computationsstateIs not suitable to undertake other functions

It seems that if you want to do some additional complex synchronous/asynchronous operations after an action is sent, the only way you can do this is when you send an action, or dispatch, which we call responsible for these complex operations: Middleware Middleware.

Middleware

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Middleware provides third-party outreach between actions that originate and actions that arrive at the reducer.

For example, if we wanted to add printing before and after sending an action, the prototype middleware would look something like this:

let next = store.dispatch
store.dispatch = function Logger(store, action) {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}

// Follow the middleware specification for currying
const Logger = store= > next= > action= > {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}

Copy the code

CreateStore can accept more parameters than reducers. One of these parameters is enhancer, which is the middleware you want to register.

/ / https://github.com/reduxjs/redux/blob/v4.0.4/src/createStore.js#L53. enhancer(createStore)(reducer, preloadedState) ...Copy the code

With that in mind, let’s take a look at how the redux source code implements store.dispatch.

/ / https://github.com/reduxjs/redux/blob/v4.0.4/src/applyMiddleware.js
export default function applyMiddleware(. middlewares) {
  return createStore= > (. args) = > {
    conststore = createStore(... args)let dispatch = () = > {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.')}const middlewareAPI = {
      getState: store.getState,
      dispatch: (. args) = >dispatch(... args) }const chain = middlewares.map(middleware= >middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}
Copy the code

As you can see, any middleware received by applyMiddleware uses map to go to the outermost layer of currying. The middlewareAPI is a simplified version of store, which guarantees that every middleware can get the same store. Next => action => {},… An array like this.

Then, using compose(function composition), string the chain from the above:

export default function compose(. funcs) {
  if (funcs.length === 0) {
    return arg= > arg
  }

  if (funcs.length === 1) {
    return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }Copy the code

Compose’s ability to compose [a, B, c] into (… args) => a(b(c(… args)))

Go back to the top, combine the middleware chains, and receive store.dispatch (understandably, this is what we need for next), which is the enhanced dispatch

dispatch = middleware1(middleware2(middleware3(store.dispatch)))
Copy the code

Combined with our middleware paradigm: Next => Action => Next (Action), store.dispatch as middleware3’s next,… , Middleware2 (Middleware3 (store.dispatch)) as the next of Middleware1, suddenly the dispatch is sublimated, but so ♂️.

(You see, you see, the core code, just a few lines, but full of flavor, there is no sense? Are you tempted? Why not open gayHub?

Of course, if you’re familiar with the React ecosystem, you might say, “React has a concept called Context, and as versions get more powerful, can I do without Redux??”

Context

The React document website does not define Context. It describes scenarios and how to use the Context.

In some cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful ‘context’ API.

Simply put, when you don’t want to pass props or state layer by layer in the component tree, you can use the Context API to pass component data across layers.

import { createContext } from "react";

export const CounterContext = createContext(null);
Copy the code

We declare a CounterContext that briefly explains how to use it, and ceateContext accepts the default.

Provider

Wrap the target component and declare value as share state

import React, { useState } from "react"
import { CounterContext } from "./context"

import App from "./App"

const Main = () = > {
    const [counter, setCounter] = useState(0)
    return (
        <CounterContext.Provider
            value={{
                counter.add:() = > setCounter(counter + 1),
                dec: () => setCounter(counter - 1)
            }}
        >
            <App />
        </CounterContext.Provider>)}Copy the code

As shown above, the Provider is wrapped around the App and provides some operations for counter.

Comsumer

Consume the value provided by the Provider

import React, { useContext } from "react";
import { CounterContext } from "./context";
import "./styles.css";

export default function App(props) {
  let state = useContext(CounterContext);

  return (
      <>.</>)}Copy the code

Use Context hooks. Make sure your React version >=16.8.

Any descendant of the App can use useContext anywhere to fetch values from Prodider.

So that’s all there is to the Context, as usual, just look at Counter and do a comparison with Redux.

Context vs Redux

Actually, there’s not much to compare.

The Context API is a simplified version of Redux. It doesn’t combine powerful Middleware to extend superpowers like Redux-Thunk or Redux-saga for complex asynchronous tasks, nor does it have developed/targeted capabilities. But if you’re just looking for a place to store your share data to avoid the nauseating props Drilling problem, the Context API is definitely worth cheering on.

react-redux

React is a UI framework. Give me a state and I’ll give you a UI view. The key now is to notify React of state updates in Redux so that it can update the UI in a timely manner.

The React team stepped in. They adapted React, and the product was React-Redux.

Provider

Wraps the target component, receiving store as share state

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import App from './pages'
import reducers from './reducers'

const store = createStore(reducers)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>.document.getElementById('root'))Copy the code

This is a standard React project entry. The Provider receives the only store provided by Redux.

connect

Connect Component to Store, giving Component the ability to use state and Dispatch actions

import { connect } from "react-redux"

const mapStateToProps = (state) = > ({
  counter: state.counter
});

const mapDispatchToProps = {
  add: () = > ({ type: 'INCREMENT' }),
  dec: () = > ({ type: 'DECREMENT'})};export default connect(mapStateToProps, mapDispatchToProps)(App)
Copy the code

In the code snippet above,

  • mapStateToPropsreceivestateTo obtaincomponentWant to value
  • mapDispatchToPropsSome claimaction creatorAnd by theconnectprovidedispatchAbility, endowcomponentdistributedactionThe ability to
  • It also receivesmergePropsandoptionsAnd other custom parameters

As usual, let’s look at Counter based on the React-Redux implementation.

Redux pain points

So just to review, when we used the example of Redux, what was the pain point?

Yes (no one answered, but I heard it in your hearts)

“Components need to actively subscribestoreThe update”

In contrast to the React-Redux demo, the intuitive feeling is no longer to subscribe where you need it, but to connect instead.

Dare to ask: “How would you implement React-Redux based on your current knowledge and the usage you just analyzed?”

Source code analysis

Yes, it must be the Context API. Let’s take a quick look at the source code to verify the guess.

Searching the entire project, we only used the only two apis provided by React-Redux, which we could quickly trace from the entrance.

Provider

React-redux uses the Context API to get store state in every corner of the app

import React, { useMemo, useEffect } from 'react'
import { ReactReduxContext } from './Context'
// Abstraction of store. Subscribe
import Subscription from '.. /utils/Subscription'

function Provider({ store, context, children }) {
  const contextValue = useMemo(() = > {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

  // Use userMemo to cache data to avoid redundant re-render
  const previousState = useMemo(() = > store.getState(), [store])

  // Notify the subscriber to respond when contectValue, previousState changes
  useEffect(() = > {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return () = > {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  
  // context nested
  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code

Regardless of the complex optimization of nested context and re-render, a Provider simply passes the received store to each component through the Context API.

connect

First, let’s be clear: The purpose of connect is to get the desired props from the Store to the Component.

When mounted, subscribe to the value of a component. After that, all updates to it will cause re-render of connected’s descendants.

This is how connect works, but we know that React has a high cost of props changes, and every time it changes, all of its descendants will follow it with re-render, so most of the code below is to optimize the huge re-render overhead.

export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory, } = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ... extraOptions } = {}) {
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect'.// used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: (name) = > `Connect(${name}) `.// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced. extraOptions, }) } }export default /*#__PURE__*/ createConnect()
Copy the code

The default export is createConnect return func, which takes a bunch of default arguments. Why bother?

(Read the previous comments carefully for the convenience of doing better testing case)

Then we move on to the internal implementation, which accepts four parameters from the user and initializes the first three with match

match

Very simple, take a factory function, and each time need to initialize the key, from back to front through the factory, any response is not empty, return (in fact, in order to accommodate the user’s input parameters, ensure format and nullation).

And then connectHOC, which is the processing core, which takes a SelectorFactory.

SelectorFactory

Depending on the value of the option.pure (default true) passed in, determine whether the props should be cached each time it is returned. This will reduce unnecessary computations and optimize performance.

connectHOC
export default function connectAdvanced(
  /* selectorFactory is a func that is responsible for returning the selector function used to compute new props from state, props, and dispatch. For example: export default connectAdvanced((dispatch, options) => (state, props) => ({ thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), }))(YourComponent) Access to dispatch is provided to the factory so selectorFactories can bind actionCreators outside of  their selector as an optimization. Options passed to connectAdvanced are passed to the selectorFactory, along with displayName and WrappedComponent, as the second argument. Note that selectorFactory is responsible for all caching/memoization of inbound and outbound props. Do not use connectAdvanced directly without memoizing results between calls to your selector, otherwise the Connect component will re-render on every state or props change. */
  selectorFactory,
  // options object:
  {
    // the func used to compute this HOC's displayName from the wrapped component's displayName.
    // probably overridden by wrapper functions such as connect()
    getDisplayName = (name) => `ConnectAdvanced(${name}) `.// shown in error messages
    // probably overridden by wrapper functions such as connect()
    methodName = 'connectAdvanced'.// REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
    // calls to render. useful for watching in react devtools for unnecessary re-renders.
    renderCountProp = undefined.// determines whether this HOC subscribes to store changes
    shouldHandleStateChanges = true.// REMOVED: the key of props/context to get the store
    storeKey = 'store'.// REMOVED: expose the wrapped component via refs
    withRef = false,

    forwardRef = false.// the context consumer to use
    context = ReactReduxContext,

    //additional options are passed through to the selectorFactory ... connectOptions } = {}) {
  if(process.env.NODE_ENV ! = ='production') {
    if(renderCountProp ! = =undefined) {
      throw new Error(
        `renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`)}if (withRef) {
      throw new Error(
        'withRef is removed. To access the wrapped instance, use a ref on the connected component')}const customStoreWarningMessage =
      'To use a custom Redux store for specific components, create a custom React context with ' +
      "React.createContext(), and pass the context object to React Redux's Provider and specific components" +
      ' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' +
      'You may also pass a {context : MyContext} option to connect'

    if(storeKey ! = ='store') {
      throw new Error(
        'storeKey has been removed and does not do anything. ' +
          customStoreWarningMessage
      )
    }
  }

  const Context = context

  return function wrapWithConnect(WrappedComponent) {
    if( process.env.NODE_ENV ! = ='production' &&
      !isValidElementType(WrappedComponent)
    ) {
      throw new Error(
        `You must pass a component to the function returned by ` +
          `${methodName}. Instead received ${stringifyComponent( WrappedComponent )}`)}const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'

    const displayName = getDisplayName(wrappedComponentName)

    constselectorFactoryOptions = { ... connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, displayName, wrappedComponentName, WrappedComponent, }const { pure } = connectOptions

    function createChildSelector(store) {
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }

    // If we aren't running in "pure" mode, we don't want to memoize values.
    // To avoid conditionally calling hooks, we fall back to a tiny wrapper
    // that just executes the given callback immediately.
    const usePureOnlyMemo = pure ? useMemo : (callback) = > callback()

    function ConnectFunction(props) {
      const [
        propsContext,
        reactReduxForwardedRef,
        wrapperProps,
      ] = useMemo(() = > {
        // Distinguish between actual "data" props that were passed to the wrapper component,
        // and values needed to control behavior (forwarded refs, alternate context instances).
        // To maintain the wrapperProps object reference, memoize this destructuring.
        const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [props.context, reactReduxForwardedRef, wrapperProps]
      }, [props])

      const ContextToUse = useMemo(() = > {
        // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
        // Memoize the check that determines which context instance we should use.
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)? propsContext : Context }, [propsContext, Context])// Retrieve the store and ancestor subscription via context, if available
      const contextValue = useContext(ContextToUse)

      // The store _must_ exist as either a prop or in context.
      // We'll check to see if it _looks_ like a Redux store first.
      // This allows us to pass through a `store` prop that is just a plain value.
      
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

      if( process.env.NODE_ENV ! = ='production'&&! didStoreComeFromProps && ! didStoreComeFromContext ) {throw new Error(
          `Could not find "store" in the context of ` +
            `"${displayName}". Either wrap the root component in a <Provider>, ` +
            `or pass a custom React context provider to <Provider> and the corresponding ` +
            `React context consumer to ${displayName} in connect options.`)}// Based on the previous check, one of these must be true
      const store = didStoreComeFromProps ? props.store : contextValue.store

      const childPropsSelector = useMemo(() = > {
        // The child props selector needs the store reference as an input.
        // Re-create this selector whenever the store changes.
        return createChildSelector(store)
      }, [store])

      const [subscription, notifyNestedSubs] = useMemo(() = > {
        if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY

        // This Subscription's source should match where store came from: props vs. context. A component
        // connected to the store via props shouldn't use subscription from context, or vice versa.
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription
        )

        // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
        // the middle of the notification loop, where `subscription` will then be null. This can
        // probably be avoided if Subscription's listeners logic is changed to not call listeners
        // that have been unsubscribed in the middle of the notification loop.
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )

        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])

      // Determine what {store, subscription} value should be put into nested context, if necessary,
      // and memoize that value to avoid unnecessary context updates.
      const overriddenContextValue = useMemo(() = > {
        if (didStoreComeFromProps) {
          // This component is directly subscribed to a store from props.
          // We don't want descendants reading from this store - pass down whatever
          // the existing context value is from the nearest connected ancestor.
          return contextValue
        }

        // Otherwise, put this component's subscription instance into context, so that
        // connected descendants won't update until after this component is done
        return {
          ...contextValue,
          subscription,
        }
      }, [didStoreComeFromProps, contextValue, subscription])

      // We need to force this wrapper component to re-render whenever a Redux store update
      // causes a change to the calculated child component props (or we caught an error in mapState)
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch,
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

      // Propagate any mapState/mapDispatch errors upwards
      if (previousStateUpdateResult && previousStateUpdateResult.error) {
        throw previousStateUpdateResult.error
      }

      // Set up refs to coordinate values between the subscription effect and the render logic
      const lastChildProps = useRef()
      const lastWrapperProps = useRef(wrapperProps)
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false)

      const actualChildProps = usePureOnlyMemo(() = > {
        // Tricky logic here:
        // - This render may have been triggered by a Redux store update that produced new child props
        // - However, we may have gotten new wrapper props after that
        // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
        // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
        // So, we'll use the child props from store update only if the wrapper props are the same as last time.
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }

        // TODO We're reading the store directly in render() here. Bad idea?
        // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
        // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
        // to determine what the child props should be.
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

      // We need this to execute synchronously every time we re-render. However, React warns
      // about useLayoutEffect in SSR, so we try to detect environment and fall back to
      // just useEffect instead to avoid the warning, since neither will run anyway.
      useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        wrapperProps,
        actualChildProps,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
      ])

      // Our re-subscribe logic only runs when the store/subscription setup changes
      useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
      )

      // Now that all that's done, we can finally try to actually render the child component.
      // We memoize the elements for the rendered child component as an optimization.
      const renderedWrappedComponent = useMemo(
        () = > (
          <WrappedComponent
            {. actualChildProps}
            ref={reactReduxForwardedRef}
          />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )

      // If React sees the exact same element reference as last time, it bails out of re-rendering
      // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
      const renderedChild = useMemo(() = > {
        if (shouldHandleStateChanges) {
          // If this component is subscribed to store updates, we need to pass its own
          // subscription instance down to our descendants. That means rendering the same
          // Context instance, and putting a different value into the context.
          return (
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>)}return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

      return renderedChild
    }

    // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
        return <Connect {. props} reactReduxForwardedRef={ref} />
      })

      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }

    return hoistStatics(Connect, WrappedComponent)
  }
}
Copy the code

It’s a lot, a lot, a lot, using hooks syntax, which makes it more complicated, but that’s okay, we’re doing it from the bottom up.

You can see that the final return is hoistStatics(Connect, WrappedComponent). This method copies the static method properties of the WrappedComponent hanging onto the resulting component, so we go to Connect.

A few lines up, connect does a layer of react. Memo around ConnectFunction based on Pure. We know this is to prevent unnecessary re-render caused by props.

ConnectFunction, the key function, returns renderedChild, and The renderedChild wraps the Memo around the renderedWrappedComponent, and it receives the actualChildProps, This is the result that we need mapStateToprops to return.

Ok, now that we know the render logic of this HOC, how does it recalculate on store updates and then trigger re-render?

For a component to re-render, it must be props or state.

Boy, we see the useReducer and saw the forceComponentUpdateDispatch, plus you listen the variable name.

NewChildProps === lastChildProps. Current newChildProps === lastChildProps. Forcibly trigger component update, yes!

So, where is checkForUpdates, and how does it sense store updates?

Originally we just missed a tiger at the beginning, useIsomorphicLayoutEffectWithArgs. UseLayoutEffect, this guy is compatible with SSR versions of the components is updated every time, we see the component rendering, then through subscription. The subscription and onStatechnage binding checkforUpdate trySubscribe, So subscription here triggers checkForUpdate every time the store changes.

It’s that simple!!

Mobx

It’s worth noting that in addition to Redux, there’s another product that’s been getting a lot of buzz in the community in recent years: Mobx.

It is a powerful, very easy to use state management tool. Even the authors of Redux have recommended it, and in many cases you can use Mobx instead of Redux.

Again, Flux, Redux, Mobx, etc., are not strongly bound to React. You can use them in any framework, hence the need for libraries like React-Redux, Mobx-React, etc.

Mobx is relatively easy to use, and it should be easy to use for people who switch from Vue to React. There are three basic points:

Create a monitored state

In general, we use Observables to create states that can be monitored, which can be objects, arrays, classes, and so on.

import { observable } from "mobx"

class Store {
  @observable counter = 0
}

const store = new Store()
Copy the code

Create views to respond to state changes

After state is created, if you’re developing an application and you need views to feel changes, MobX updates views in a minimal way, and it’s incredibly efficient.

Take the React Class Component as an example.

import React from 'react'
import {observer} from 'mobx-react'

@observer
class Counter extends React.Component {
    render() {
        return (
            <div>
                <div>{this.props.state.counter}</div>
                <button onClick={this.props.store.add}>Add</button>
                <button onClick={this.props.store.dec}>Dec</button>
                <button onClick={()= > (this.props.store.counter = 0)}>clear</button>
            </div>)}}export default Counter
Copy the code

Triggering state change

Modify the code that creates monitor status in section 1

import { observable, action } from "mobx"

class Store {
  @observable counter = 0
  @action add = () = > {
    this.counter++
  }

  @action dec = () = > {
    this.counter--
  }
}

const store = new Store()
Copy the code

Combined with the view in the previous section, both add and Dec algorithms call methods provided by Store, which makes sense.

What’s scary is that clear directly assigns a value to state’s counter, which is also successful. Moreover, the view is updated in time. I can’t help recalling clear in flux chapter, which is even more frightening and makes people look backward.

This is the magic of Mobx. Just like VUE, mobx registers listeners with a Proxy to implement dynamic and timely responses.

To satisfy React users’ fears of this state being out of control, it also provides an API to restrict this operation, requiring action to modify the Store.

enforceAction

Specifies that only action can change a store.

import { configure } from 'mobx'

configure({enforceAction: true})
Copy the code

provider

It also provides providers similar to React-Redux. The descendant component uses Inject, receives state injected by the Provider, and uses observer to connect the React component and MOBx state. Achieve the effect of real-time corresponding state change.

Others, such as Autorun, reaction, and when computed, are triggered automatically when conditions are met, and you can learn more about them yourself.

Same old rule, go through a Counter to see what happens.

Mobx vs Redux

Through the above simple introduction and demo experience, I believe you also have a general feeling, let’s simply compare it with Redux.

Freedom, which is both Mobx’s strength and its weakness, can be a recipe for disaster when a project is large and involves multiple people.

Cough, point to can, understand all understand.