Sample code is available here

Last time we looked briefly at Redux (see article here), today we’ll implement our own react-Redux with React.

Create a project

Create a new project with create-react-app, remove the redundant parts under SRC, and add your own files as follows:

# Modified directory structure
++ src
++++ component
++++++ Head
-------- Head.js
++++++ Body
-------- Body.js
++++++ Button
-------- Button.js
---- App.js
---- index.css
---- index.js

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

// App.js
import React, { Component } from 'react';
import Head from './component/Head/Head';
import Body from './component/Body/Body';
export default class App extends Component {
  render() {
    return (
      <div className="App"> <Head /> <Body /> </div> ); }}# Head.js
import React, { Component } from 'react';
export default class Head extends Component {
  render() {
    return (
      <div className="head">Head</div> ); }}# Body.js
import React, { Component } from 'react';
import Button from '.. /Button/Button';
export default class Body extends Component {
  render() {
    return (
      <div>
        <div className="body">Body</div> <Button /> </div> ); }}# Button.js
import React, { Component } from 'react';
export default class Button extends Component {
  render() {
    return (
      <div className="button">
        <div className="btn"> change head</div> <div className="btn"> change body</div> </div>); }}Copy the code

The above code is not complicated, let’s write some styles for them, and finally look at the effect:

We can see that we wrote all the text in head and body, which is not good for our development, because we can’t change these values. Now we want to change the corresponding text when we click the bottom button, but we can’t do that with the current code. Of course, we could do this through a series of props passes, but that would be pretty tedious because it involves passing values not only to parent components but also to sibling components’ children. At this point, we need a global shared store, so that we can access it easily anywhere and complete data acquisition and modification very conveniently.

Second, the context

React provides the Context API to handle such nested scenarios. React 16.3 and later, the context has been updated. Context provides us with a globally shared state that can be easily accessed from the top-level component’s store in any descendant component. We modify our code like this:

# App.js
import PropTypes from 'prop-types'; .export default class App extends Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext () {
    const state = {
      head: 'I'm the global head',
      body: 'I'm a global body',
      headBtn: 'to modify the head',
      bodyBtn: 'modify body'
    }
    return { store: state };
  }
  render() {... }}# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Head extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){ const { store } = this.context; this.setState({ ... store }) }render() {
    return (
      <div className="head">{this.state.head}</div> ); }}# body.js
import PropTypes from 'prop-types'; .export default class Body extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){ const { store } = this.context; this.setState({ ... store }) }render() {
    return (
      <div>
        <div className="body">{this.state.body}</div> <Button /> </div> ); }}# Button.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Button extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){ const { store } = this.context; this.setState({ ... store }) }render() {
    return (
      <div className="button">
        <div className="btn">{this.state.headBtn}</div>
        <div className="btn">{this.state.bodyBtn}</div> </div> ); }}Copy the code

Looking at the page, we can see that the global store in the top-level component is already accessed by descendant components:

Specify the data type in the top-level component with childContextTypes. 2. Set the data in the top-level component using getChildContext. 3. Specify data types with contextTypes in descendant components. 4. Get data in descendant components using the context parameter.

Through the above steps, we created a globally shared store. You may be wondering why we defined the _upState method in the descendant component instead of writing it directly into the lifecycle. This question will be left unanswered and you will see why in the following sections. Now, let’s combine this store with redux, which we wrote earlier (see the previous article on Redux, here at 👇).

Third, the React – story

Let’s create a new redux folder and complete our Redux (see the previous article for the following code implications) :

# index.js
export * from './createStore';
export * from './storeChange';

# createStore.js
export const createStore = (state, storeChange) => {
  const listeners = [];
  let store = state || {};
  const subscribe = (listen) => listeners.push(listen);
  const dispatch = (action) => {
    const newStore = storeChange(store, action);
    store = newStore; 
    listeners.forEach(item =>  item())
  };
  const getStore = () => {
    return store;
  }
  return { store, dispatch, subscribe, getStore }
}

# storeChange.js
export const storeChange = (store, action) => {
  switch (action.type) {
    case 'HEAD':
      return { 
        ...store,  
        head: action.head
      }
    case 'BODY':
      return { 
        ...store,
        body: action.body
      }
    default:
      return { ...store }
  }
}
Copy the code

With the above code, we have completed Redux, where the createstore.js code is almost exactly the same as the previous article, with minor modifications, so you can take a look for yourself. Now let’s combine this with context:

# App.js. import { createStore, storeChange } from'./redux';

exportdefault class App extends Component { static childContextTypes = { store: PropTypes.object, dispatch: PropTypes. Func, subscribe: PropTypes. Func, getStore: PropTypes. Func}getChildContext () {
    const state = {
      head: 'I'm the global head',
      body: 'I'm a global body',
      headBtn: 'to modify the head',
      bodyBtn: 'modify body'} const {store, dispatch, subscribe, getStore} = createStore(state,storeChange)return{store, dispatch, subscribe, getStore}; }render() {... }}# Head.js.export default class Head extends Component {
  static contextTypes = {
    store: PropTypes.object,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  ...
  componentWillMount(){
    const { subscribe } = this.context;
    this._upState();
    subscribe(() => this._upState())
  }
  _upState(){ const { getStore } = this.context; this.setState({ ... getStore() }) }render() {... }}# Body.js.exportDefault class Body extends Component {static contextTypes = {// same as head.js}...componentWillMount(){// same as head.js}_upState(){// same as head.js}render() {
    return (
      <div>
        <div className="body">{this.state.body}</div> <Button /> </div> ); }}# Button.js.export default class Button extends Component {
  static contextTypes = {
    store: PropTypes.object,
    dispatch: PropTypes.func,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){// same as head.js}_upState(){// same as head.js}render() {... }}Copy the code

In the above code, we create a global store using the createStore method. Store, Dispatch, and subscribe are passed through the context so that each descendant component can easily obtain these global attributes. Finally, we use setState to change the state of each descendant component, and add a listening function to SUBSCRIBE. When store changes, let the component get store again and re-render. Here we see the use of _upState, which allows us to easily add callbacks to store changes. When we look at the page, we see that the page is not abnormal, and the context can still be accessed on subsequent pages. In this way, does it mean that our union is successful? Hold on, let’s change the data and try it out. We modify button.js to add click events to the Button to change the store:

# Button.js. changeContext(type){
    const { dispatch } = this.context;
    dispatch({ 
      type: type,
      head: 'I'm the modified data.'
    });
  }
  render() {
    return (
      <div className="button">
        <div className="btn" onClick={() => this.changeContext('HEAD')}>{this.state.headBtn}</div>
        <div className="btn" onClick={() => this.changeContext('BODY')}>{this.state.bodyBtn}</div>
      </div>
    );
  }
Copy the code

Click the button and we see:

Four, optimization

1, connect

Redux/React/react/redux/React/react/redux/React 1) There is a lot of repetitive logic in each descendant component, we get store in context, then update their state, and also add listening events. 2) The code is almost not reusable in various descendant components, and the dependency on context is too strong. Let’s say your colleague wants to use the Body component, but the context is not set in his code. The Body component is not available.

We can solve these problems with higher-order components, where we can encapsulate the repetitive logic of the code, and we’ll call that encapsulating method connect. It’s just a name, you don’t have to worry about it, you can call it AAA if you want. Let’s create a new connect file in the redux folder:

# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
  class Connect extends Component {
    render() {return (
        <div className="connect"> <Comp /> </div> ); }}return Connect;
}
Copy the code

As you can see, CONNECT is a higher-order component that takes a component and returns the processed component. We use the Head component to verify that this higher-order component is available:

# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '.. /.. /redux';
class Head extends Component {
 ...
}
export default connect(Head);
Copy the code

We can see that connect is doing what it’s supposed to do, and has successfully wrapped a div around the Head component:

# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object,
      dispatch: PropTypes.func,
      subscribe: PropTypes.func,
      getStore: PropTypes.func
    }
    constructor (props) {
      super(props)
      this.state = {};
    }
    componentWillMount(){
      const { subscribe } = this.context;
      this._upState();
      subscribe(() => this._upState())
    }
    _upState(){ const { getStore } = this.context; this.setState({ ... getStore() }) }render() {return (
        <div className="connect">
          <Comp {...this.state} />
        </div>
      );
    }
  }
  return Connect;
}

# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '.. /.. /redux';
class Head extends Component {
  render() {
    return (
      <div className="head">{this.props. Head}</div> // From props); }}export default connect(Head);
Copy the code

We see that the Head component is very streamlined, we only need to care about the specific business logic, and any operations related to the context are moved to connect. We transform the Body and Button components in the same way:

# Body.js. class Body extends Component {render() {
    return (
      <div>
        <div className="body">{this.props.body}</div> <Button /> </div> ); }}export default connect(Body)

# Button.js. class Button extends Component { changeContext(type, value){ const { dispatch } = this.context; // The context does not exist.type: type,
      head: value
    });
  }
  render() {
    return (
      <div className="button">
        <div className="btn" onClick={() => this.changeContext('HEAD'.'I'm a changed number one')}>{this.props.headBtn}</div>
        <div className="btn" onClick={() => this.changeContext('HEAD'.'I am changed data 2')}>{this.props.bodyBtn}</div> </div> ); }}export default connect(Button)
Copy the code

There’s nothing wrong with refreshing the page, everything seems fine, but when we click the button, an error occurs. We found that in Button, dispatch is not available, and the only source of data we have now is props. In Connect, we don’t deal with Dispatch, so let’s go ahead and modify our Connect:

# Button.js. const { dispatch } = this.props; // Take the value from props...# connect.js.export const connect = (Comp) => {
  class Connect extends Component {
   ...
    constructor (props) {
      super(props)
      this.state = {
        dispatch: () => {}
      };
    }
    componentWillMount(){ const { subscribe, dispatch } = this.context; SetState ({dispatch}) this._upstate (); subscribe(() => this._upState()) } ... }return Connect;
}
Copy the code

Now it seems that everything has been settled. Let’s review what we did:

1) We have encapsulated CONNECT and entrusted him with all the operations related to CONNECT. 2) We modified descendant components to get data from props instead of relying on the context.

Now, when we look at the problems that we had before, we see that we’ve solved them pretty well. But is that really enough? When we looked at the code in Connect, we found that all of the PropTypes we had written were rigid, inflexible and not very good for development. After all, each component had to get different data. If WE could make Connect accept another parameter, To specify PropTypes that’s great. Based on this requirement, we continue to modify our code:

# connect.js.exportconst connect = (Comp, propsType) => { class Connect extends Component { static contextTypes = { store: PropTypes.object, dispatch: PropTypes.func, subscribe: PropTypes.func, getStore: PropTypes.func, ... propsType } ... }return Connect;
}

# Head.js. const propsType = { store: PropTypes.object, }export default connect(Head, propsType);

Copy the code

Above, we reconfigured Connect to take two parameters, write down some fixed attributes to be passed, and then add propsType we defined separately inside each component.

2, the Provider

We can see that in all of the descendant components, context operations have been isolated, but in app.js, there is still context related content. In fact, the context is only used in the App to store the store so that future components can get data from it. So, we can do state lifting entirely through the container component, taking the dirty work out of the App component and promoting it into the new container component. We just pass it the store that we need to put in the context. Based on the previous idea, we will create a new Provider under the Redux folder and remove all the non-business code from the App:

# Provider
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createStore, storeChange } from '.. /redux';
export class Provider extends Component {
  static childContextTypes = {
    store: PropTypes.object,
    dispatch: PropTypes.func,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  getChildContext () {
    const state = this.props.store;
    const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
    return { store, dispatch, subscribe, getStore };
  }
  render() {return (
      <div className="provider">{this.props.children}</div> ); }}# App.js .export default class App extends Component {
  render() {
    return (
      <div className="App"> <Head /> <Body /> </div> ); }}# index.js. import { Provider } from'./redux'
const state = {
  head: 'I'm the global head',
  body: 'I'm a global body',
  headBtn: 'to modify the head',
  bodyBtn: 'modify body'
}
ReactDOM.render(
  <Provider store={state}>
    <App />
  </Provider>, 
  document.getElementById('root'));Copy the code

The revamped App components are also very clean. We define the global store in index.js, and insert the container component Provider into the context, so that all subsequent components can be easily obtained, while in the App component, we only need to pay attention to the specific business logic.

The last word

This article uses some simple code examples to create our own react-Redux library. Of course, the above code is too simple and has many problems. It is also a bit different from the common react-Redux library. If there is any incorrect description, welcome to correct!