What is the react – redux

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

Let’s first consider how our React project would work with Redux if we didn’t use React-Redux.

For each component that needs to be used with Redux, we need to do the following:

  • In the componentstoreThe state of
  • Listening to thestoreWhen the state changes, refresh the component
  • Remove listening for state changes when a component is uninstalled.

As follows:

import React from 'react';
import store from '.. /store';
import actions from '.. /store/actions/counter';
Reducer is combineReducer({counter,... * the structure of the state for * {}) * counter: {number: 0}, *... *} * /
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: store.getState().counter.number
        }
    }
    componentDidMount() {
        this.unsub = store.subscribe((a)= > {
            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(); }}Copy the code

If there are many components in our project that need to be used with Redux, they all need to write this logic repeatedly. Obviously, we need to find a way to reuse this part of the logic, or we’ll look stupid. We know that high-order components in React allow reuse of logic.

[Counter code] used in this article (github.com/YvetteLau/B…) Clone the myReact-redux /counter code first.

Logic reuse

Create a react-redux folder in the SRC directory. Create subsequent files in this folder.

Create the connect.js file

File created under react-redux/components:

We’ll write the repetitive logic in 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((a)= > {
                this.setState({
                    this.setState(store.getState());
                });
            });
        }
        componentWillUnmount() {
            this.unsub();
        }
        render() {
            return (
                <WrappedComponent {. this.state} {. this.props} / >)}}}Copy the code

One small problem is that although the logic is repetitive, each component needs different data and should not pass all state to the component, so we want connect to be able to tell connect what the required state content is when we call connect. In addition, the component may need to change its state, so you also need to tell CONNECT which actions it needs to dispatch, otherwise Connect won’t know which actions to bind to you.

To do this, we added two new parameters: mapStateToProps and mapDispatchToProps, which are responsible for telling the CONNECT component what state is required and what action is to be delivered.

MapStateToProps and mapDispatchToProps

We know what mapStateToProps and mapDispatchToProps are for, but so far we don’t know what format these parameters should be passed to Connect to use.

import { connect } from 'react-redux'; ./ / the use of the connect
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Copy the code
  • MapStateToProps tells CONNECT what state the component needs to bind to.

    The mapStateToProps needs to pick the state the component needs from the entire state, but when we call connect, we don’t get store, but we do get store from inside connect. So, We define mapStateToProps as a function, call it inside connect, pass it state from store, and then pass the result returned by the function as a property to the component. Component from this.props.XXX. Therefore, the format for mapStateToProps should look something like this:

    // Pass store.getState() to mapStateToProps
    mapStateToProps = state= > ({
        number: state.counter.number
    });
    Copy the code
  • MapDispatchToProps tells CONNECT which actions the component needs to bind to.

    Recall that the component dispatches actions: store.dispatch({actions.add(2)}). After the connect wrap, we still need to be able to deliver the action, which must be something like this.props.XXX().

    Call this.props. Add (2) to dispatch store.dispatch({actions. Add (2)}). (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))
        }
    }
    Copy the code

    Just like mapStateToProps, when we call connect, we don’t get store.dispatch, so we need to design mapDispatchToProps as a function called inside connect, This will pass it store.dispatch. So, mapStateToProps should look like this:

    // Pass store.dispacth to mapDispatchToProps
    mapDispatchToProps = (dispatch) = > ({
        add: (num) = > {
            dispatch(actions.add(num))
        },
        minus: (num) = > {
            dispatch(actions.minus(num))
        }
    })
    Copy the code

Now that we know the format of mapStateToProps and mapDispatchToProps, it’s time to further improve Connect.

The 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((a)= > {
                    const mappedState = mapStateToProps(store.getState());
                    //TODO does a shallow comparison, and does not setState if the state has not changed
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {. this.props} {. this.state} {. this.mappedDispatch} / >)}}}}Copy the code

As we know, connect is provided as a method of the React-Redux library, so it is not possible to import a store directly from connect.js. The store should be passed in by an application that uses React-Redux. React passes data in one of two ways: through properties props or through the context object Context. Components wrapped in connect are distributed across the application, and context is designed to share data that is “global” to a component tree.

We need to put store on the context so that all descendants of the root component can get store. Of course we could write code for this in our own application, but obviously this code is repeated in every application. So we’ll encapsulate this inside the React-Redux as well.

Here, we’re using the old Context API (we’re using the old Context API because of the code we implemented for the React-Redux 4.x branch).

Provider

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

Create the provider.js file

File created under react-redux:

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) allows the Provider to wrap only one child. We just return this.props. Children */
        return this.props.children
    }
}
Copy the code
Create a new index.js file

File created in 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
}
Copy the code

The use of the Provider

To use it, we simply import the Provider and pass the store to the 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>)}}Copy the code

At this point, the source code and usage of the Provider are clear, but the corresponding connect needs to be modified. For versatility, we need to fetch the store from the context instead of importing it.

The 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 is the same part of the code as 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;
                // 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((a)= > {
                    const mappedState = mapStateToProps(this.store.getState());
                    //TODO does a shallow comparison. If the state has not changed, setState is not required
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {. this.props} {. this.state} {. this.mappedDispatch} / >)}}}}Copy the code

Use connect to associate the data in the Counter and 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);
Copy the code

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;
Copy the code

At this point, our React-Redux library is ready to use, but there are many details to be worked out:

  • So the definition of mapDispatchToProps is a little bit tricky to write, if you remember bindActionCreators from redux, with this method we were able to pass actionCreator to connect, The transformation is then performed within CONNECT.

  • The PropType rules for connect and stores in providers can be extracted to avoid code redundancy

  • MapStateToProps and mapDispatchToProps can provide default values. The default value of mapStateToProps is state => ({}). Unassociated state;

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

  • So far, we just passed store.getState() to mapStateToProps, but it is possible that when filtering the required state, we will need to use the properties of the component itself, so we can pass the properties of the component to mapStateToProps as well. It also passes its own property to mapDispatchToProps for the same reason.

The connect version 3.0

We’ve pulled out the Store PropType rule and put it in the utils/ STO0 0.

The shallow comparison code is in the utils/ shallowequal.js file. The general shallow comparison function is not listed here.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '.. /utils/storeShape';
import shallowEqual from '.. /utils/shallowEqual';
/** * mapStateToProps default unassociated state * mapDispatchToProps Default to dispatch => ({dispatch}), pass the 'store.dispatch' method as a property to the component */
const defaultMapStateToProps = state= > ({});
const defaultMapDispatchToProps = dispatch= > ({ dispatch });

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(! mapStateToProps) { mapStateToProps = defaultMapStateToProps; }if(! mapDispatchToProps) {// If mapDispatchToProps is null/undefined/false... , the default value is used
        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 () 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 actionCreator object
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = this.store.subscribe((a)= > {
                    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} / >)}}}}Copy the code

Now, our Connect allows mapDispatchToProps to be a function or actionCreators object, and if mapStateToProps and mapDispatchToProps are either default or null, Can also perform well.

One problem, however, is that connect returns all the component names as CONNECT, which is not easy to debug. So we can add displayName to it.

The connect version 4.0

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '.. /utils/storeShape';
import shallowEqual from '.. /utils/shallowEqual';
/** * By default, mapDispatchToProps is not associated with state * mapDispatchToProps by default, the default value is dispatch => ({dispatch}), Pass the 'store.dispatch' method as a property 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) {// If mapDispatchToProps is null/undefined/false... , the default value is used
        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 () 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 actionCreator object
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = store.subscribe((a)= > {
                    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} / >)}}}}Copy the code

React-redux is basically implemented by now, but the code is not perfect. For example, the ref is missing, and this. State and this.mappedDispatch are recalcuated when the props of the component changes, without further performance optimization. You can build on that.

The react-Redux trunk branch has been rewritten using hooks, a new version of the code will be published later if time is available.

Finally, using our own writing the react – the story and the story write Todo demo, function is normal, the code in the https://github.com/YvetteLau/Blog myreact redux/Todo.

Attached is how to use the old and new context APIS:

context

There are currently two versions of the Context API. The older API will be supported in all 16.x releases, but will be removed in future releases.

The context API (new)

const MyContext = React.createContext(defaultValue);
Copy the code

Create a Context object. When React renders a component subscribed to the Context object, the component reads the current Context value from the closest matching Provider in the component tree.

Note: The defaultValue parameter takes effect only if the component’s tree does not match the Provider.

use
Context.js

First we create the Context object

import React from 'react';

const MyContext = React.createContext(null);

export default MyContext;
Copy the code
Root Component (pannel.js)
  • Set the content you want to share to<MyContext.Provider>valueIn (context value)
  • Child components were<MyContext.Provider>The parcel
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 (
            // The attribute name must be value
            <MyContext.Provider value={this.state.theme}>
                <Content />
            </MyContext.Provider>)}}Copy the code
Descendant components (content.js)

Class components

  • defineClass.contextType: static contextType = ThemeContext;
  • throughthis.contextTo obtain<ThemeContext.Provider>valueThe content of (i.econtextValue)
/ / class components
import React from 'react';
import ThemeContext from './Context';

class Content extends React.Component {
    // Once the contextType is defined, the contents of the themecontext. Provider value can be retrieved from this.context
    static contextType = ThemeContext;
    render() {
        return (
            <div style={{color: `2px solidThe ${this.context.color} `}} >
                //....
            </div>)}}Copy the code

Function component

  • The child element is wrapped in<ThemeContext.Consumer>
  • <ThemeContext.Consumer>The child element of is a function, the input parametercontextValue (ProviderTo provide thevalue). This is a{color: XXX}
import React from 'react';
import ThemeContext from './Context';

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

The context API (old)

use
  • Defines the root componentchildContextTypes(validationgetChildContextType returned)
  • definegetChildContextmethods
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 (
            // The attribute name must be value
            <>
                <Content />
            </>)}}Copy the code
Descendant components (content.js)
  • Defines descendant componentscontextTypes(Declare and validate the type of state you want to get)
  • This. Context is used to get the context content passed in.
import React from 'react';
import PropTypes from 'prop-types';

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

Reference links:

  • React-redux source: github.com/reduxjs/rea…
  • React-redux (2): connect juejin.cn/post/684490…
  • Write a React-redux from scratch juejin.cn/post/684490…

Pay attention to the public number, join the technical exchange group