This is the third part of the react-Redux series. In this series, we will write A react-Redux wheel from scratch. In this series, we will analyze the source code of some classic wheels and implement them from scratch. Dom-diff, Webpack, Babel, KAO, Express, Async /await, jquery, Lodash, RequireJS, Lib-flexible and other front-end classic wheel implementation, each chapter source code is hosted on Github, Learn to Make Wheels Together (1) : Start from scratch Write A Promise that Promises/A+ Promises learn to Make Wheels Together (2) : Start from Scratch Write A Redux Learn how to Build Wheels at Github

preface

In the last chapter we wrote about redux. When redux is combined with React, react-Redux is usually used for convenience. This library is optional. In a real project, there should be a trade-off between using Redux directly and react-Redux. The latter, while convenient, requires mastery of additional apis and compliance with its component separation specifications. The use of React-Redux will not be covered much in this article, but the focus will remain on the source code implementation. If you do not know how to use it, you can read related articles to learn.

Recommended article: Redux tutorial: React-redux usage

All the code in this article is in github code repository, you can click here to view the code in this article, also welcome everyone star~

start

context

Before we talk about react-redux, let’s talk about the context in react. js. The context in react. js has always been seen as an unstable, dangerous feature that could be removed from the official documentation, but it’s very convenient to use. For example, we have a huge tree of components, and when we don’t use Redux we want to change a state and make all the components work. We need to pass down props layer by layer. But it’s very easy to have the context. When a component puts some state into its context, all of its children can access that state directly without passing it through an intermediate component.

For example, here is a component tree:


If the component hierarchy is deep, passing down values with props is a disaster.

We thought it would be nice if the tree could share a state store globally, and we could go to the state store when we needed it, without having to manually pass it in.

Context Class Index extends Component {static childContextTypes = {userInfo: proptypes.object}constructor() {
        super()
        this.state = { 
            userInfo: {
                name:"Xiao Ming", id: 17}}}getChildContext() {
        return { userInfo: this.state.userInfo }
    }

    render() {
        return ( <div >
                    <Header/>
                </div>
        )
    }
}

class Header extends Component {
    render() {
        return ( <div>
                <Title/>
            </div>
        )
    }
}
class Title extends Component {
    static contextTypes = {
        title: PropTypes.object
    }
    render() {// No matter how deep the component hierarchy is, child components can get state directly from the context propertyreturn(< h1 > {welcome this. Context. The userInfo. Name} < / h1 >)}}Copy the code

Above, we define userInfo on the root Index component and mount it to the Index context, and then we can get the title state directly from the context no matter how many layers of child components there are below.

So why use redux to manage global state when context is so convenient?

Because the data in the context can be accessed at will, it can be modified at will, causing the program to run unpredictably. This is why context is always deprecated, whereas Redux, while cumbersome to use, makes the behavior of modifying data predictable and traceable, because in Redux you have to perform certain permitted changes through Dispatch, and you have to specify what you want to do in the action.

So can we combine the best of both so that we can manage global state safely and easily?

React-Redux

// root.js
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import userReducer from 'reducers/userReducer'
import Header from 'containers/header'
const store = createStore(userReducer)

export default class Root extends Component {
    render() {
        return (<div>
                    <Header></Header>
                </div>
        );
    };
}
ReactDOM.render( <Provider store = { store } >
                        <Root/>
                </Provider>, 
document.getElementById('root'));


//containers/header.js
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as userinfoActions from 'actions/userinfo.js';
import fetch from 'isomorphic-fetch'

class Header extends Component {
    constructor() {
        super();
        this.state = {
            username:""}}componentDidMount(){
        this.getUserInfo()
    }
    getUserInfo(){
        fetch("/api/pay/getUserInfo")
            .then(response => {
                return response.json()
            })
            .then(json =>{
                this.props.userinfoActions.login(data);
                this.setState({username: data.username});
            })
            .catch(e => {
                console.log(e)
            })
    }
    render() {return{this.state.username} </div>; }}function mapStateToProps(state) {
    return { userinfo: state.userinfo }
}

function mapDispatchToProps(dispatch) {
    return {
        userinfoActions: bindActionCreators(userinfoActions, dispatch)
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Header)


// reducers/userReducer.js
export default function userinfo(state = {}, action) {
    switch (action.type) {
        case "USERINFO_LOGIN":
            return action.data
        default:
            return state
    }
}


// actions/useraction.js
export function login(data) {
    return {
        type: "USERINFO_LOGIN",
        data
    }
}
Copy the code

The above is a simple scenario. After entering the page to obtain the user information, the user name in the user information is displayed in the header of the page. Because the user information is used by multiple components, not only the header component, it is shared in redux.

The function mapStateToProps and mapDispatchToProps pass it to connect. The function mapDispatchToProps passes it to connect. Let’s talk about what these things do and how to implement them.

Provider

Let’s look at Provider. Provider is a higher-order component. We can see that when we use it, we wrap it around the root component, and we pass in store as its props. Then mount the store to its context so that all of its children can share the global state. Let’s see how:

// Provider.js
import React, { Component } from 'react';
import propTypes from 'prop-types';
export default class Provider extends Component {
    static childContextTypes = {
        store: propTypes.object.isRequired
    }
    getChildContext() {
        return { store: this.props.store };
    }
    render() {
        returnthis.props.children; }}Copy the code

This is easier to implement. Write a component Provider, attach a store to the Provider context, and then use it around the root component. Since the Provider is the parent of the original root component, it becomes the true root component. All of the following sub-components can access the Store through the context. The Provider component uses the context feature to solve the problem that every component in the project needs to import store to use Redux, greatly increasing the convenience.

connect

We can, of course, because store is already mounted to the context of the root component, and all of its children can access store through the context, and then use the state in store, In addition, the store dispatch is used to submit the action to update the status, but this is still inconvenient, because each component is too dependent on the context, resulting in the logic of the component dealing with the store and the logic of the component itself are coupled together, making the component unable to reuse.

Ideally, a component rendering relies only on props and its own state passed in from the outside, and does not rely on any external data, which is the most reusable component. How to separate the logic of a component dealing with store from the logic of the component itself? The answer is to use higher-order components. We wrap the original business components (such as header, list, etc.) with another layer of components, so that the parts of the component dealing with Store are placed in the outer layer, and the inner layer is only responsible for its own logic. The outer component communicates with the inner component via props, so that the place where the component interacts with the Store is like a shell separated from the component entity. We can reuse the component entity anywhere just by changing the shell. The Connect function does that.


MapDispatchToProps is the second parameter to the connect function that maps UI component parameters to the Store. dispatch method. That is, it defines which user actions should be passed to the Store as actions. It can be a function or an object.

These two functions can be simply understood as the requirements of the inner component entity and the outer shell component. The component entity uses mapStateToProps to tell the shell component which state it wants on the store, and then the shell component gets it from the store and passes it to the component entity in the form of props. MapDispatchToProps similarly.

When we use the connect other general wrote export default connect (mapStateToProps mapDispatchToProps) (Header), So the connect function receives mapStateToProps and mapDispatchToProps, and then returns a function whose parameters receive the component to be wrapped, and then returns the packaged component. A friend might ask, why not directly connect (mapStateToProps mapDispatchToProps, Header), still have to is divided into two functions to write, because the React – official story is so of the design, In my opinion, the author wants to improve the reusability of connect. We won’t go into the design of connect, but we will focus on its code implementation.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import propTypes from 'prop-types';
export default function connect(mapStateToProps, mapDispatchToProps) {
    return function(WrapedComponent) {class ProxyComponent extends Component {static contextTypes = {store: propTypes.object } constructor(props, context) { super(props, context); this.store = context.store; this.state = mapStateToProps(this.store.getState()); }componentWillMount() {
                this.store.subscribe(() => {
                    this.setState(mapStateToProps(this.store.getState()));
                });
            }
            render() {
                let actions = {};
                if (typeof mapDispatchToProps == 'function') {
                    actions = mapDispatchToProps(this.store.disaptch);
                } else if (typeof mapDispatchToProps == 'object') {
                    actions = bindActionCreators(mapDispatchToProps, this.store.dispatch); } // Shell components render real component entities inside and pass in props the state in the store that the business components want to trigger and the actions that they want to triggerreturn<WrapedComponent {... this.state } {... actions} /> } }returnProxyComponent; }}Copy the code

So what does connect do?

  1. It first receives mapStateToProps, mapDispatchToProps and returns a function, which receives a component.
  2. Declare a shell component ProxyComponent and get the Store object through the context.
  3. The desired state of the component entity is then retrieved and stored on the shell component’s state via the getState method of the store object passed in from constructor via the mapStateToProps function.
  4. In the shell component componentWillMount lifecycle register the callback function when store state changes: Store changes, synchronously update its own state to the latest state, consistent with the state on store.
  5. Encapsulate as functions any related actions that a component submits using Dispatch. MapDispatchToProps is a function or an object, for example, mapDispatchToProps.
function mapDispatchToProps(dispatch) {
    return {
        userinfoActions: bindActionCreators(userinfoActions, dispatch)
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)
Copy the code

Export default Connect (mapStateToProps,… userinfoActions )(Header)

We want to make sure that when submitting an action within a component entity, the user can use this.props. XXX (), regardless of whether the mapDispatchToProps is a function or an Action Creator object. Without directly touching the Store’s dispatch method.

So we need redux’s bindActionCreators method, the second article in this series, to learn how to build a wheel. This method, described in Redux, allows us to submit actions as methods and automatically dispatch the corresponding actions. So we can see that when the user passes in a function, the user uses bindActionCreators inside the mapDispatchToProps function to convert the ActionCreator into a method, but if you pass in the ActionCreator object directly, So we used bindActionCreators inside Connect to convert the incoming ActionCreator into a one-by-one method, which means that if the user doesn’t do this, then react-Redux will do it for you.

  1. The next step is to pass all properties on the shell component state and all actions encapsulated as functions from the previous step to the component entity via the props method.
  2. Finally, we return the wrapped component, and now we can use this.props. Username inside the component entity to get the state on the store. Or use this. Props. UserinfoActions. Login (data) to submit the action, at this time to deal with the store’s logic and the component’s own logic separate, internal components entities can be reuse.

The last

React-redux provides a simple and compact implementation of react-Redux by encapsulating the Provider component and connect method. This code is available on Github. If you like it, please click here. Welcome to pay attention ~