React requires data state management. Because of component-based development, it is inevitable to encounter cross-level data communication. It is more convenient to maintain and elegant to replace layers of props with a unified data state management. In the author’s opinion, Redux is not necessary for React. As long as it implements data state management and provides sufficient open capabilities such as hooks, observers, middleware, etc., React can be combined with React. Simply speaking, tools that only implement data management + observer are sufficient to cooperate with React. As for why Redux is used, it’s probably because it’s so widely used. This article will examine how the two work together and parse the react-Redux library that the community has encapsulated. If you are interested in redux, read the previous article and start from zero to one





First connect the component to Redux

React developers know that React advocates one-way data flow. By changing the state, render can be retriggered to update the view. For componentized development mode, the state data is passed to the child component props to pass the data, and the setState encapsulated by the parent component is changed to update the view. So it’s all about state and how to change state. So we came up with the connection:

  • Connect store data to state
  • Listen for store updates to change state

Here’s how to do it:

// redux module
import {createStore} from 'redux'
const initState = {
    text: 'hello world'
}
function reducer(state = initState, action) {
    switch(action.type) {
       case 'reverse_text':
            state.text = state.text.reverse();
            return state;
        default:
            return state;
    }
}
export const store = createStore(reducer, initState)
Copy the code
import React from 'react' import { store } from './redux'; export default class Header extends React.Component { constructor(props) { super(props); this.state = { text: store.getState().text } } componentDidMount() { store.subscribe(() => { console.log(store.getState()) this.setState({ text: store.getState().text }) }) } render() { return ( <div className="hello-header"> {this.state.text} </div> ); }}Copy the code

The principle is very simple, initialize the data read from the store, call SUBSCRIBE, listen for store changes, reset state, let’s see how to update the Header component in other components

import React from 'react'
import { store } from './redux';

export default class Bottom extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
      console.log(store)
  }

  render() { 
    return ( 
      <div className="hello-bottom">
        <button onClick={() => {
            store.dispatch({
                type: 'reverse_text'
            })
        }}>改变redux试试看</button>
      </div>
     );
  }
}
 
Copy the code
import React from 'react' import './App.css'; import Header from './header'; import Bottom from './bottom'; export default class App extends React.Component { constructor(props) { super(props); } render() { return ( <div className="hello-app"> <Header /> <Bottom /> </div> ); }}Copy the code

See, this is a nice way to solve the data communication problems of peer components, but it’s not enough. Because the example above is so short, it’s hard to pass layers of props if the component hierarchy is too deep. You need to drop the stores in one by one

More reasonable connection method

The above design is certainly useful, but let’s imagine that a large complex project, components at least dozens of thousands, the need for unified management of the data is certainly very much, for the need to use the store data of many components, it is impossible for each of these components to read store and register monitoring it. Therefore, we need to design a more reasonable and non-invasive connection method. Based on this scenario, we boldly propose the following functions.

  • The topmost component mounts the store uniformly, making the entire store accessible to all subsequent levels of child components
  • Provide a more friendly API for components that need to connect to the Store, rather than intrusive constructor and lifecycle writing

Uniformly mount stores

Seemingly simple enough, we wrote the following code:

import React from 'react' import './App.css'; import Header from './header'; import Bottom from './bottom'; import { store } from './redux'; export default class App extends React.Component { constructor(props) { super(props); this.state = { store: store.getState() } } render() { const {store} = this.state; return ( <div className="hello-app"> <Header store={store} /> <Bottom store={store} /> </div> ); }}Copy the code

This works fine, but if Header&Bottom itself is a higher-order component, it’s inevitable that the store needs to be constantly transmitted from the top layer through props, which is awkward. React-redux provides a component called Provider, which is easy to use

import React from 'react' import './App.css'; import Header from './header'; import Bottom from './bottom'; import { store } from './redux'; import { Provider } from 'react-redux'; export default class App extends React.Component { constructor(props) { super(props); this.state = { store: store.getState() } } render() { const {store} = this.state; return ( <Provider store={store}> <Header /> <Bottom /> </Provider> ); }}Copy the code

So can perfect solution layer upon layer transfer store dilemma, what to do, open the source!

import React, { Component } from 'react' import { ReactReduxContext } from './Context' import Subscription from '.. /utils/Subscription' const ReactReduxContext = React.createContext(null) class Provider extends Component { constructor(props) { super(props) const { store } = props this.notifySubscribers = this.notifySubscribers.bind(this) const subscription = new Subscription(store) subscription.onStateChange = this.notifySubscribers this.state = { store, subscription } this.previousState = store.getState() } componentDidMount() { this._isMounted = true this.state.subscription.trySubscribe() if (this.previousState ! == this.props.store.getState()) { this.state.subscription.notifyNestedSubs() } } componentWillUnmount() { if (this.unsubscribe) this.unsubscribe() this.state.subscription.tryUnsubscribe() this._isMounted = false } componentDidUpdate(prevProps) { if (this.props.store ! == prevProps.store) { this.state.subscription.tryUnsubscribe() const subscription = new Subscription(this.props.store) subscription.onStateChange = this.notifySubscribers this.setState({ store: this.props.store, subscription }) } } notifySubscribers() { this.state.subscription.notifyNestedSubs() } render() { const Context = this.props.context || ReactReduxContext return ( <Context.Provider value={this.state}> {this.props.children} </Context.Provider> ) } } export default ProviderCopy the code

The latest react-Redux has been updated to the Hook version, for the convenience of everyone to understand the old version 7.0, although there are many code, there are two key points

  • Receives the Store from props and sets it to its own state
  • React.createContext

CreateContext provides the following functions. You can also view the official document React. CreateContext

All right, let’s start non-invasively with the components that need to connect – connect

Why don’t you see how it works, and then work backwards from where it’s used

import React from 'react' import { store } from './redux'; import { connect } from 'react-redux' class Header extends React.Component { constructor(props) { super(props); } render() { return ( <div className="hello-header"> {this.props.text} <button onClick={this.props.reverse}>reverse</button> </div> ); } } const mapStateToProps = (state) => { return { text: state.text } } const mapDispatchToProps = ( dispatch, ownProps ) => { return { reverse: () => { dispatch({ type: 'reverse_text', }); }}; } const HeaderWrapper = connect( mapStateToProps, mapDispatchToProps )(Header) export default HeaderWrapperCopy the code

The connect API defines the data that our component needs to subscribe to from the Store in mapStateToProps, and the methods that our component needs to update the store in mapDispatchToProps. We only need to focus on the business of the component itself, which is very loose and does not need coupling. The more elegant it looks on the surface, the more complex and sophisticated its design and implementation must be. This is true of Connect, which I personally find the most complex and difficult part of React-Redux to understand. But we can do the simplest version of it. Based on the above code, we can reasonably infer the following 2 functions.

  • Connect returns a higher-order component based on the passed mapStateToProps, mapDispatchToProps, and Header, and injects the properties and methods of the mapStateToProps, mapDispatchToProps configuration
  • The returned higher-order component can communicate with the Store.

It can be implemented simply as follows

function connectHoc (mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) { return function wrapWithConnect (wrappedComponent) { function connectFunction (props)  { const [_, SetState] = useState(0) const store = useContext(defaultContext) const store = useContext(defaultContext useEffect(() => { return store.subscribe(update) }, []) function update () { setState(times => ++times) } const stateProps = mapStateToProps(store.getState()) const dispatchProps = mapDispatchToProps(store.dispatch) const allProps = { ... props, ... stateProps, ... dispatchProps } return <wrappedComponent {... PureComponent return React. Memo (ConnectFunction)}}Copy the code

We need CONNECT to remember the data we need, but it’s pretty crude. The components that we CONNECT will all be updated when the Store updates them, not when the Store updates the component mapStateToProps. Component updates are made only when the Store updates the data. So how do we do that

To compare the data before and after the update, first write some methods to judge the data

Function is(x, y) {if (x === y) {return x! == 0 || y ! == 0 || 1 / x === 1 / y } else { return x ! == x && y ! Export function shallowEqual(objA, objB) {if (is(objA, objB)) return true if (typeof objA! == 'object' || objA === null || typeof objB ! == 'object' || objB === null ) { return false } const keysA = Object.keys(objA) const keysB = Object.keys(objB) if (keysA.length ! == keysb.length) return false for (let I = 0; i < keysA.length; i++) { if ( ! Object.prototype.hasOwnProperty.call(objB, keysA[i]) || ! Is (objA[keysA[I]], objB[keysA[I]])) {return false}} return true} b) { return a === b }Copy the code
Import {shallowEqual, strictEqual} from './equals' // ownProps) { return { ... ownProps, ... stateProps, ... Function mergedPropsFactory() {let hasOnceRun = false // The closure save is executed // Let stateProps = NULL let dispatchProps = NULL let ownProps = null let mergedProps = null return (newStateProps, newDispatchProps, newOwnProps) => {// For the first time return the new props to render if (! hasOnceRun) { stateProps = newStateProps dispatchProps = newDispatchProps ownProps = newOwnProps mergedProps = mergeProps(stateProps, dispatchProps, OwnProps) hasOnceRun = true return mergedProps} // If (shallowEqual(stateProps, newStateProps) && shallowEqual(ownProps, newOwnProps)) { stateProps = newStateProps dispatchProps = newDispatchProps ownProps = newOwnProps } else { stateProps =  newStateProps dispatchProps = newDispatchProps ownProps = newOwnProps mergedProps = mergeProps(stateProps, dispatchProps, ownProps) } return mergedProps } }Copy the code

The final Connect

import React from 'react'; import mergedPropsFactory from './mergeProps.js'; import { useState, useRef, useMemo, useContext, useEffect } from 'react' import {Context} from './provider'; export default function connectHoc(mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) { return function WrapWithConnect(WrappedComponent) { function ConnectFunction(props) {  const [_, SetState] = useState(0) const store = useContext(Context) store useEffect(() => {return Subscribe (update) // Subscribe store}, []) function update() {// If (cacheAllProps. Current ===) {// If (cacheAllProps mergeProps(mapStateToProps(store.getState()), cacheDispatchProps.current, CacheOwnProps. Current)) return setState(times => ++times)} const mergeProps use a method that checks the before and after properties once if and only if the props are changed = useMemo(() => (mergedPropsFactory()), []) const stateProps = mapStateToProps(store.getState()) const dispatchProps = mapDispatchToProps(store.dispatch) const allProps = mergeProps(stateProps, dispatchProps, Const cacheAllProps = useRef(null) const cacheOwnProps = useRef(null) const cacheStatePros = UseRef (null) const cacheDispatchProps = useRef(null) useEffect(() => {// cacheAllProps cacheStatePros.current = stateProps cacheDispatchProps.current = dispatchProps cacheOwnProps.current = props }, [allProps]) return <WrappedComponent {... Return React. Memo (ConnectFunction)}}Copy the code

Here’s how it works: At the very beginning

Try clicking on the Header component

Try clicking the Bottom component

The trigger header component changes the corresponding store props, but the Bottom component does not update, and vice versa.

Finally, we’ll talk about container components and presentation components

The reason why I put it last is because I don’t think this is a hardcore content, and it’s not necessary to memorize one or two more concepts. As mentioned earlier, the react workflow is all about state and how to change state. React-redux is just a combination of this method and the Redux application. Connect the component with connect and let it listen. After listening, pass the data to the simple component for presentation through props. That’s all. There’s no secret.

Summary.

React-redux is a combination of redux and React. React-redux is a combination of redux and React. React-redux is a combination of redux and React, and react-Redux is a combination of redux and React. React-redux uses subscription, which is a different way to identify and remember Props before and after. Finally, I would like to share a feeling that both Redux and React-Redux are based on one or two very small use scenarios and functions, but they are full of design in order to apply gracefully to many scenarios. It’s worth thinking about, and that’s the difference between code design that works and code design that works. Don’t forget to give this article a thumbs up if it helps. All of this code is available on Github at Mini-React-Redux