[React Series] Implement a react-redux by Hands

  Front end, javascript, Programmer, react.js

What is react-redux

react-reduxYesreduxOfficialReactBinding library. It helps us connect the UI layer and the data layer. The purpose of this article is not to introducereact-reduxThe use of, but to begin to realize a simplereact-reduxI hope it can help you.

First of all, think about it, if you don’t use itreact-reduxOurreactHow to Combine in the ProjectreduxFor development?

Each need is related toreduxCombined components, we all need to do the following:

  • Gets in the componentstoreStatus in
  • MonitorstoreWhen the state changes, refresh the component
  • When the component is unloaded, monitoring for state changes is removed.

As follows:

import React from 'react';
import store from '../store';
import actions from '../store/actions/counter';
/**
 * reducer 是 combineReducer({counter, ...})
 * state 的结构为 
 * {
 *      counter: {number: 0},
 *      ....
 * }
 */
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: store.getState().counter.number
        }
    }
    componentDidMount() {
        this.unsub = store.subscribe(() => {
            if(this.state.number === store.getState().counter.number) {
                return;
               }
            this.setState({
                number: store.getState().counter.number
            });
        });
    }
    render() {
        return (
            <div>
                <p>{`number: ${this.state.number}`}</p>
                <button onClick={() => {store.dispatch(actions.add(2))}}>+</button>
                <button onClick={() => {store.dispatch(actions.minus(2))}}>-</button>
            <div>
        )
    }
    componentWillUnmount() {
        this.unsub();
    }
}

If there are many components in our project that need to be connected withreduxWhen used in combination, these components all need to write the logic repeatedly. Obviously, we need to find ways to reuse this part of the logic, otherwise it will appear that we are stupid. We know that,reactMedium and high-order components can realize logic reuse.

As used in this articleCounterCode inhttps://github.com/YvetteLau/Bloghit the targetmyreact-redux/counterChina, it is suggested that firstcloneCode, of course, if you think this article is good, give star encouragement.

Logical multiplexing

InsrcCreate a new one under the directoryreact-reduxFolder, subsequent files are newly created in this folder.

Js file

File created inreact-redux/componentsUnder folder:

We will repeat the logic of writingconnectChina.

import React, { Component } from 'react';
import store from '../../store';

export default function connect (WrappedComponent) {
    return class Connect extends Component {
        constructor(props) {
            super(props);
            this.state = store.getState();
        }
        componentDidMount() {
            this.unsub = store.subscribe(() => {
                this.setState({
                    this.setState(store.getState());
                });
            });
        }
        componentWillUnmount() {
            this.unsub();
        }
        render() {
            return (
                <WrappedComponent {...this.state} {...this.props}/>
            )
        }
    }
}

There is a small problem, although this logic is repeated, but the data required by each component is different, should not pass all the state to the component, so we hope to callconnect, can inform the required status contentconnect. In addition, the component may need to modify the state, so also want to tellconnectWhich actions does it need to dispatch, otherwiseconnectCan’t know which actions to bind to you.

To this end, we add two new parameters:mapStateToPropsAndmapDispatchToPropsWhich are responsible for tellingconnectRequired by componentstateContents and actions to be distributed.

MapStateToProps and mapDispatchToProps

We knowmapStateToPropsAndmapDispatchToPropsWhat is the function of the, but so far, we are not clear, the two parameters should be a what kind of format to pass toconnectUse it.

import { connect } from 'react-redux';
....
//connect 的使用
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
  • MapStateToProps toldconnect, the state in which the component needs to be bound.

    mapStateToPropsYou need to pick the state that the component needs from the entire state, but when callingconnectWe can’t get itstoreHoweverconnectInternal is availablestoreYes, to this end, we willmapStateToPropsIs defined as a function in theconnectCalling it internally willstorehit the targetstatePass it, and then pass the result returned by the function to the component as a property. Through in the assemblythis.props.XXXTo get. Therefore,mapStateToPropsThe format of should look like this:

    //pass store.getState () to mapStateToProps
    mapStateToProps = state => ({
    number: state.counter.number
    });
  • MapDispatchToProps toldconnectThe action that the component needs to bind.

    Recall that dispatch actions in components:store.dispatch({actions.add(2)}).connectAfter packaging, we still need to be able to distribute the action, definitelythis.props.XXX()Such a format.

    For example, increasing the counter, callingthis.props.add(2), is need to distributestore.dispatch({actions.add(2)}), thereforeaddProperty, the corresponding content is(num) => { store.dispatch({actions.add(num)}) }. The properties passed to the component look like this:

    {
    add: (num) => {
    store.dispatch(actions.add(num))
    },
    minus: (num) => {
    store.dispatch(actions.minus(num))
    }
    }

    AndmapStateToPropsLike, in callingconnectWe can’t get itstore.dispatch, so we also need tomapDispatchToPropsDesigned as a function inconnectInternal call so that you canstore.dispatchPass it on. So,mapStateToPropsIt should be in the following format:

    //pass store.dispacth to mapDispatchToProps
    mapDispatchToProps = (dispatch) => ({
    add: (num) => {
    dispatch(actions.add(num))
    },
    minus: (num) => {
    dispatch(actions.minus(num))
    }
    })

So far, we have made it clearmapStateToPropsAndmapDispatchToPropsIt is time to further improve the format ofconnectHere we go.

Connect version 1.0

import React, { Component } from 'react';
import store from '../../store';

export default function connect (mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            constructor(props) {
                super(props);
                this.state = mapStateToProps(store.getState());
                this.mappedDispatch = mapDispatchToProps(store.dispatch);
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState());
                    //TODO 做一层浅比较,如果状态没有改变,则不setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

We know that,connectYes asreact-reduxLibrary method, so we can’t directly in theconnect.jsTo importstoreThisstoreShould be used byreact-reduxThe application of the.reactThere are two types of data transfer in: through the propertypropsOr through context objectscontextThroughconnectPackaged components are distributed in the application whilecontextThe design goal is to share data that is “global” to a component tree.

We need to putstoreput in a certain positioncontextIn this way, all descendant components under the root component can be obtainedstore. This part of the content, of course, we can write the corresponding code in the application, but obviously, these codes are repeated in each application. Therefore, we also encapsulate this part of the content inreact-reduxInside.

Here, we use the oldContext APITo write (because we implemented the react-redux 4.x branch of code, so we use the old version of the context API).

Provider

We need to provide oneProviderComponent, its function is to receive the applicationstoreTo hang it on thecontext, so that all its descendant components can be obtained through the context objectstore.

新建 Provider.js 文件

File created inreact-redux/componentsUnder folder:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Provider extends Component {
    static childContextTypes = {
        store: PropTypes.shape({
            subscribe: PropTypes.func.isRequired,
            dispatch: PropTypes.func.isRequired,
            getState: PropTypes.func.isRequired
        }).isRequired
    }
    
    constructor(props) {
        super(props);
        this.store = props.store;
    }

    getChildContext() {
        return {
            store: this.store
        }
    }

    render() {
        /**
         * 早前返回的是 return Children.only(this.props.children)
         * 导致Provider只能包裹一个子组件,后来取消了此限制
         * 因此此处,我们直接返回 this.props.children
         */
        return this.props.children
    }
}
新建一个 index.js 文件

File created inreact-reduxUnder the directory:

This document only does one thing, namelyconnectAndProviderExport

import connect from './components/connect';
import Provider from './components/Provider';

export {
    connect,
    Provider
}

Use of Provider

When in use, we only need to introduceProvider, willstorePass toProvider.

import React, { Component } from 'react';
import { Provider } from '../react-redux';
import store from './store';
import Counter from './Counter';

export default class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <Counter />
            </Provider>
        )
    }
}

So far,ProviderThe source code and use of has been explained clearly, but the correspondingconnectAlso need to make some changes, in order to versatility, we need fromcontextGet up therestoreTo replace the previous import.

Connect version 2.0

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default function connect(mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            //PropTypes.shape 这部分代码与 Provider 中重复,因此后面我们可以提取出来
            static contextTypes = {
                store: PropTypes.shape({
                    subscribe: PropTypes.func.isRequired,
                    dispatch: PropTypes.func.isRequired,
                    getState: PropTypes.func.isRequired
                }).isRequired
            }

            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(this.store.getState());
                this.mappedDispatch = mapDispatchToProps(this.store.dispatch);
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState());
                    //TODO 做一层浅比较,如果状态没有改变,则无需 setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

UseconnectcorrelationCounterAndstoreData in.

import React, { Component } from 'react';
import { connect } from '../react-redux';
import actions from '../store/actions/counter';

class Counter extends Component {
    render() {
        return (
            <div>
                <p>{`number: ${this.props.number}`}</p>
                <button onClick={() => { this.props.add(2) }}>+</button>
                <button onClick={() => { this.props.minus(2) }}>-</button>
            </div>
        )
    }
}

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

const mapDispatchToProps = (dispatch) => ({
    add: (num) => {
        dispatch(actions.add(num))
    },
    minus: (num) => {
        dispatch(actions.minus(num))
    }
});


export default connect(mapStateToProps, mapDispatchToProps)(Counter);

Js is defined as follows:

import { INCREMENT, DECREMENT } from '../action-types';

const counter = {
    add(number) {
        return {
            type: INCREMENT,
            number
        }
    },
    minus(number) {
        return {
            type: DECREMENT,
            number
        }
    }
}
export default counter;

So far, ourreact-reduxThe library is ready for use, but there are many details to deal with:

  • mapDispatchToPropsThe definition of is a bit troublesome to write and is not concise enough.
    Do you still rememberreduxhit the targetbindActionCreatorsWith this method, we can allow deliveryactionCreatortoconnect, and then inconnectInternal conversion.
  • connectAndProviderhit the targetstoreThePropTypeRules can be extracted to avoid code redundancy.
  • mapStateToPropsAndmapDispatchToPropsDefault values can be provided
    mapStateToPropsThe default value isstate => ({}); Not relatedstate;

    mapDispatchToPropsThe default value for isdispatch => ({dispatch}), willstore.dispatchMethod is passed as an attribute to the wrapped attribute.

  • At present, we have only passedstore.getState()tomapStateToPropsHowever, it is very likely that you are filtering what you needstateWhen the user clicks the button, it needs to be processed according to the component’s own attributes. therefore, the component’s own attributes can also be passed to themapStateToProps, for the same reason, also passed its own attributes tomapDispatchToProps.

Connect version 3.0

We willstoreThe PropType rule of theutils/storeShape.jsIn the file.

The code for shallow comparison is placed inutils/shallowEqual.jsIn the document, the general shallow comparison function is not listed here. If you are interested, you can read the following code directly.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps 默认不关联state
 * mapDispatchToProps 默认值为 dispatch => ({dispatch}),将 `store.dispatch` 方法作为属性传递给组件
 */
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if (!mapDispatchToProps) {
        //当 mapDispatchToProps 为 null/undefined/false...时,使用默认值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = {
                store: storeShape
            };
            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(this.store.getState(), this.props);
                if (typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
                } else {
                    //传递了一个 actionCreator 对象过来
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState(), this.props);
                    if (shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

Now, ourconnectAllowmapDispatchToPropsIs it a function or is itactionCreatorsObject, inmapStateToPropsAndmapDispatchToPropsThe default or isnullHe can also perform well.

However, there is another problem.connectAll component names returned areConnect, not easy to debug. So we can add itdisplayName.

Connect version 4.0

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps 缺省时,不关联state
 * mapDispatchToProps 缺省时,设置其默认值为 dispatch => ({dispatch}),将`store.dispatch` 方法作为属性传递给组件
 */ 
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if(!mapDispatchToProps) {
        //当 mapDispatchToProps 为 null/undefined/false...时,使用默认值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = storeShape;
            static displayName = `Connect(${getDisplayName(WrappedComponent)})`;
            constructor(props) {
                super(props);
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(store.getState(), this.props);
                if(typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
                }else{
                    //传递了一个 actionCreator 对象过来
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState(), this.props);
                    if(shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

So far,react-reduxWe basically realized it, but this code is not perfect, for example,refMissing problem, component’spropsChange, recalculatethis.stateAndthis.mappedDispatch, no further performance optimization, etc. You can proceed further on this basis.

react-reduxThe code for the trunk branch is already in usehooksRewrite, if you have time later, you will output a new version of code analysis.

Finally, use our own writingreact-reduxAndreduxPreparedTodoDemo of, normal function, code inhttps://github.com/YvetteLau/Bloghit the targetmyreact-redux/todoDown.

Attach new and oldcontext APIHow to use:

context

There are currently two versions ofcontext API, the old API will be supported in all 16.x versions, but will be removed in future versions.

Context API (new)

const MyContext = React.createContext(defaultValue);

Create aContextObject. WhenReactRender a subscription to thisContextObject, this component will match the one closest to itself from the component tree.ProviderThe current is read from thecontextValue.

Note: Only if there is no match in the tree where the component is locatedProviderWhen, itdefaultValueThe parameter will not take effect.

使用
Context.js

First create the Context object

import React from 'react';

const MyContext = React.createContext(null);

export default MyContext;
根组件( Pannel.js )
  • Set the content to be shared at<MyContext.Provider>ThevalueMedium (i.e. context value)
  • The subcomponent is<MyContext.Provider>package
import React from 'react';
import MyContext from './Context';
import Content from './Content';

class Pannel extends React.Component {
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 属性名必须叫 value
            <MyContext.Provider value={this.state.theme}>
                <Content />
            </MyContext.Provider>
        )
    }
}
子孙组件( Content.js )

Class component

  • DefinitionClass.contextType:static contextType = ThemeContext;
  • viathis.contextObtain<ThemeContext.Provider>InvalueThe content of (i.e.contextValue)
//类组件
import React from 'react';
import ThemeContext from './Context';

class Content extends React.Component {
    //定义了 contextType 之后,就可以通过 this.context 获取 ThemeContext.Provider value 中的内容
    static contextType = ThemeContext;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}>
                //....
            </div>
        )
    }
}

Function component

  • Child element wrapped in<ThemeContext.Consumer>In
  • <ThemeContext.Consumer>The child element of is a function, and it is a parametercontextValue (ProviderProvidedvalue)。 Here is{color: XXX}
import React from 'react';
import ThemeContext from './Context';

export default function Content() {
    return (
        <ThemeContext.Consumer>
            {
                context => (
                    <div style={{color: `2px solid ${context.color}`}}>
                        //....
                    </div>
                )
            }
        </ThemeContext.Consumer>
    )
}

Context API (old)

使用
  • The that defines the root componentchildContextTypes(VerificationgetChildContextThe type returned)
  • DefinitiongetChildContextMethod
根组件( Pannel.js )
import React from 'react';
import PropTypes from 'prop-types';
import Content from './Content';

class Pannel extends React.Component {
    static childContextTypes = {
        theme: PropTypes.object
    }
    getChildContext() {
        return { theme: this.state.theme }
    }
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 属性名必须叫 value
            <>
                <Content />
            </>
        )
    }
}
子孙组件( Content.js )
  • A that defines descendant componentscontextTypes(Declare and verify the type of state to obtain)
  • Context can be used to obtain the passed context content.
import React from 'react';
import PropTypes from 'prop-types';

class Content extends React.Component {
    static contextTypes = PropTypes.object;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}>
                //....
            </div>
        )
    }
}

Reference link: