[Deep react-redux] Manual implementation of a react-redux

What is react-redux

React-redux is the official React binding library of redux. It helps us connect the UI layer to the data layer. The purpose of this article is not to introduce the use of react-redux, but to implement a simple react-redux, hoping to help you.

First of all, think about how to develop our react project with Redux if we don't use react-redux.

For each component that needs to be used in conjunction with redux, we need to do the following things:

  • Get the state in the store in the component
  • Monitor state changes in the store and refresh components when state changes
  • When a component is uninstalled, the monitoring of state changes is removed.

As follows:

import React from 'react';
import store from '../store';
import actions from '../store/actions/counter';
/**
 * reducer It's combineReducer({counter,...})
 * state The structure is 
 * {
 *      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 used in conjunction with redux, then these components need to rewrite these logic. Obviously, we need to find ways to reuse this part of the logic, otherwise it will look silly to us. We know that high-order components in react can achieve logical reuse.

The Counter code used in this article is in myreact-redux/counter in https://github.com/YvetteLau/Blog. clone code is recommended first. Of course, if you think this article is good, give a star encouragement.

Logic Reuse

Create a new react-redux folder under the src directory, in which subsequent files are created.

Create connect.js file

Files are created under the react-redux/components folder:

We will repeat the logic in write connect.

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 the logic is repetitive, the data required by each component is different. We should not pass all the States to the component, so we hope that when we call connect, we can tell the connection what state we need. In addition, the component may need to modify the state, then also tell connect what actions it needs to dispatch, otherwise connect can not know which actions to bind to you.

To this end, we add two new parameters: mapStateToProps and mapDispatchToProps, which are responsible for telling the connect component the required state content and the actions to be dispatched.

mapStateToProps and mapDispatchToProps

We know what the roles of mapStateToProps and mapDispatchToProps are, but so far, it is not clear what format these two parameters should be passed to connect for use.

import { connect } from 'react-redux';
....
//Use of connect
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
  • mapStateToProps tells connect that components need to be bound.

    MapStateToProps needs to select the state required by the component from the whole state, but we can't get the store when we call connect, but store can be obtained inside connect. To this end, we define mapStateToProps as a function, call it inside connect, pass the state in store to it, and then return the result of the function as a function. Properties are passed to components. The component is obtained by this.props.XXX. Therefore, the format of mapStateToProps should be similar to the following:

    //Pass store.getState() to mapStateToProps
    mapStateToProps = state => ({
        number: state.counter.number
    });
  • mapDispatchToProps tells connect that components need to bind actions.

    Recall that an action is dispatched in a component: store.dispatch({actions.add(2)}). After the connect ion is packaged, we still need to be able to dispatch the action, which must be a format like this.props.XXX().

    For example, when the counter is increased and this.props.add(2) is called, store.dispatch({actions.add(2)}) needs to be dispatched, so the add attribute corresponds to (num) => {store. dispatch ({actions. add (num)}). The attributes passed to the component are similar to the following:

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

    Like mapStateToProps, we can't get store.dispatch when we call connect, so we need to design mapDispatchToProps as a function to call inside connect, so we can pass store.dispatch to it. So mapStateToProps 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've figured out the formats of mapStateToProps and mapDispatchToProps, and it's time to further improve connect ion.

connect 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 makes a shallow comparison. If the state does not change, it does not set State.
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

As we know, connect is provided as a method of react-redux library, so it is impossible to import store directly in connect.js. This store should be imported by application using react-redux. There are two kinds of data transfer in react: through attribute props or through context of context object, components wrapped by connection are distributed in applications, and context is designed to share data that is "global" for a component tree.

We need to put the store on the context so that all descendant components under the root component can access the store. This part of the content, of course, we can write the corresponding code in the application, but obviously, these codes are repetitive in each application. So we also encapsulate this part in react-redux.

Here, we use the old Context API to write (since we implemented the react-redux 4.x branch code, we used the old context API).

Provider

We need to provide a Provider component whose function is to receive the store passed by the application and hang it on the context so that its descendants can access the store through the context object.

New Provider.js file

Files are created under the react-redux/components 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() {
        /**
         * Early returns were return Children.only(this.props.children)
         * This led Provider to wrap only one subcomponent, which was later lifted.
         * So here we go straight back to this.props.children.
         */
        return this.props.children
    }
}
Create a new index.js file

Files are created in the react-redux directory:

This file does only one thing, which is to export connect and Provider

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

export {
    connect,
    Provider
}

Use of Provider

When used, we just need to introduce Provider and pass the store to Provider.

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>
        )
    }
}

At this point, the source code and use of Provider have been clearly explained, but the corresponding connect ion also needs some modifications. In order to be universal, we need to get the store from the context, instead of the previous import.

connect 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 {
            //The PropTypes. shapes part of the code is repeated with the Provider, so we can extract it later.
            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;
                //In the source code, store.getState() is given to 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 makes a shallow comparison. If the state does not change, no setState is required.
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

Use connect to associate Counter with data in the store.

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);

store/actions/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, our react-redux library is available, but there are many details to be addressed:

  • The definition of map Dispatch To Props is a bit cumbersome and not concise enough.
    Do you remember the bindAction Creators in redux? With this method, we can allow action creators to be passed to connect and then converted within connect.
  • The ProType rules of store s in connect and Provider can be extracted to avoid code redundancy
  • mapStateToProps and mapDispatchToProps can provide default values
    The default value of mapStateToProps is state =>({}); it is not associated with state;

    The default value of mapDispatchToProps is dispatch =>({dispatch}), which passes the store.dispatch method as an attribute to the wrapped attribute.

  • At present, we only pass store.getState() to mapStateToProps, but it is likely that when filtering the required state s, we need to process them according to the properties of the component itself. Therefore, we can pass the properties of the component itself to mapStateToProps, and for the same reason, we can also pass our own properties to mapDispatchToProps.

connect 3.0

We extract the store's ProType rules and place them in the utils/storeShape.js file.

The shallow comparison code is placed in utils/shallowEqual.js file. The general shallow comparison function is not listed here. If you are interested, you can read the code directly.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps Default not associated with state
 * mapDispatchToProps The default value is dispatch =>({dispatch}), passing the `store.dispatch'method as an attribute to the component
 */
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if (!mapDispatchToProps) {
        //When mapDispatchToProps is 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;
                //In the source code, store.getState() is given to this.state.
                this.state = mapStateToProps(this.store.getState(), this.props);
                if (typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
                } else {
                    //Pass in an action creator object
                    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, our connect ion allows mapDispatchToProps to be a function or actionCreators object, which also performs well when mapStateToProps and mapDispatchToProps default or null.

However, another problem is that all component names returned by connect are Connect, which is not easy to debug. So we can add displayName to it.

connect 4.0

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps By default, no state is associated
 * mapDispatchToProps By default, set its default value to dispatch =>({dispatch}) and pass the `store.dispatch'method as an attribute to the component
 */ 
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) {
        //When mapDispatchToProps is 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);
                //In the source code, store.getState() is given to this.state.
                this.state = mapStateToProps(store.getState(), this.props);
                if(typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
                }else{
                    //Pass in an action creator object
                    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-redux is basically implemented, but the code is not perfect, such as the problem of ref loss, component props change, recalculate this.state and this.mappedDispatch, no further performance optimization. You can proceed further on this basis.

The code of the react-redux backbone branch has been rewritten using hooks, and a new version of code parsing will be output later if there is time.

Finally, Todo's demo is written using our own react-redux and redux. The function is normal. The code is under myreact-redux/todo in https://github.com/YvetteLau/Blog.

Attach the use of the new and old context API s:

context

There are currently two versions of context 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 a Context object. When React renders a component that subscribes to the Context object, the component reads the current context value from the matching Provider closest to itself in the component tree.

Note: The defaultValue parameter of a component will only take effect if it does not match the Provider in the tree in which it resides.

Use
Context.js

First create the Context object

import React from 'react';

const MyContext = React.createContext(null);

export default MyContext;
Root component (Pannel.js)
  • The content to be shared is set in the value of <MyContext.Provider> (that is, the context value)
  • Subcomponents are wrapped in <MyContext.Provider>.
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 (
            // Property name must be called value
            <MyContext.Provider value={this.state.theme}>
                <Content />
            </MyContext.Provider>
        )
    }
}
Descendant components (Content.js)

Class Components

  • Define Class. contextType: static contextType = ThemeContext;
  • Get the content of value in <ThemeContext.Provider> through this.context (that is, context value)
//Class Components
import React from 'react';
import ThemeContext from './Context';

class Content extends React.Component {
    //After defining contextType, you can get the content in ThemeContext.Provider value through this.context
    static contextType = ThemeContext;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}>
                //....
            </div>
        )
    }
}

Functional components

  • The child elements are wrapped in <ThemeContext.Consumer>.
  • The sub-element of <ThemeContext.Consumer> is a function that takes into account the context value (value provided by Provider). 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)

Use
  • Define the child ContextTypes of the root component (verify the type returned by the getChildContext)
  • Define the getChildContext method
Root component (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 (
            // Property name must be called value
            <>
                <Content />
            </>
        )
    }
}
Descendant components (Content.js)
  • Define contextTypes for descendant components (declare and validate the type of state to be acquired)
  • Through this.context, you can get the context content passed in.
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:

Tags: Javascript React Attribute github

Posted on Wed, 09 Oct 2019 16:12:24 -0700 by nickiehow