preface

In the process of learning React, you will encounter Redux and React-Redux. If you are not familiar with Redux, you may wonder why there is Redux. First, let’s take a look at the use of Redux and some jokes in the process. Let’s see why react-Redux came into being

A story,

Before we start, here’s something to keep in mind

Redux is a well-known JavaScript state management container

In other words, Redux can be used with React, JS and Vue

1.1 Design Idea

  • ReduxIs to store the entire application state in a file calledstoreWhere there is a state treestate tree
  • Components are available throughstore.dispatchDistributing behavioractionstore.storeI’m not going to modify it directlystateIt is written by the userreducerTo generate newstateAnd returns tostore
  • Other components through subscriptionstoreTo refresh your view

1.2 Three Principles

  • The entire application has only one store, and its internal State tree stores the state of the entire application
  • State is read-only and can only be modified by distributing actions. In order to describe how actions change state, we need to write a reducer pure function
  • The design of a single data source facilitates communication between React components and enables unified state management

1.3 createStroe

Learn about the Redux API by writing a Counter Counter in action

1.3.1 store

// ./src/store.js
import {createStroe} from 'react'

function reudcer(){}
let store = createStore(reudcer)
Copy the code

The createStore method creates a store and needs to pass the reducer parameter (ps: below). The Store is an object and the following methods can be called

  • Store.getstate () to get the latest state tree
  • Store.dispatch (), dispatch action
  • Store.subscribe (), the change of state in the subscription store

1.3.2 reducer

Reducer must be a pure function that accepts state and action. State is the old state and cannot be directly modified. New state needs to be generated and returned according to different action.type

// ./src/store.js
import {createStroe} from 'react'
export const ADD = 'ADD'
export const MINUS = 'MINUS'
function reducer (state = {count: 0}, action) {
  console.log('action', action); // {type: 'xxx'}
  switch(action.type) {
    case ADD:
      return {count: state.count + 1}
    case MINUS:
      return {count: state.count - 1}
    default:
      return state
  }
}

let store = createStore(reudcer)
export default store
Copy the code

Note that in the code above, we set the initial value of state {count: 0}, and then use the exported store in the Counter component

1.3.3 getState, Dispatch,subscribe

// ./src/components/Counter.jsx
import React from 'react'
import store from '.. /store'

class Counter extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      number: store.getState().count
    }
  }
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={()= > store.dispatch({type: 'ADD'})}>+</button>
      <button onClick={()= > store.dispatch({type: 'MINUS'})}>-</button>
    </div>}}export default Counter
Copy the code

In the Counter component, you can get the latest state through store.getState(). Click the button, and the action will be sent to the store through store.dispatch (ps: Please note that action is an object and must have type attribute), the current state will be passed to reducer within the store to generate a new state to update the state. Unfortunately, the number on the page has not changed

It can be seen that the Reducer function has received the action, and the state in the store has changed at this time, and the page is not updated because Counter did not subscribe to the state change in the store. Add the following code into the code

class Counter extends React.Component{
  componentDidMount () {
    this.unSubscribe = store.subscribe(() = > {
      this.setState({
        number: store.getState().count
      })
    })
  }
  componentWillUnmount () {
    this.unSubscribe && this.unSubscribe()
  }
}
Copy the code

Subscribe is implemented using store.subscribe, which takes a function that executes when state changes in store, and returns a function that unsubscribes.

At this point, the Counter component is basically implemented. Maybe some of you noticed that the console output came out when your app first loaded

action {type: "@@redux/INIT1.s.m.m.c.n"}
Copy the code

Action {type: “@@redux/ init1.s.m.m.c.n “}

For those familiar with the publish-subscribe model, Redux uses publish-subscribe internally. Next, let’s try to implement a crude version of Redux

1.3.4 Implement createStroe by handwriting


function createStore(reducer){
	let state
  const listeners = []
  
  // Return the latest state
  function getState () {
  	return state
  }
  
  Distributed / / action
  function dispatch(action){
  	state = reducer(state, action)
    listeners.forEach(listener= > listener())
  }
  
  // Subscribe, return unsubscribe function
  function subscribe(listener){
  	listeners.push(listener)
    return function () {
    	const index = listeners.indexOf(listener)
      listeners.splice(index, 1)}}// Get the default state value
  dispatch({type: "@@redux/INIT1.s.m.m.c.n"})
  
  // Return store, an object
  return {
  	getState,
    dispatch,
    subscribe
  }
}

export default createStore
Copy the code

Through testing, our humble version of Redux has implemented the functionality of the Counter component

1.4 bindActionCreators

1.4.1 Principle and Use

In the Counter component, we dispatch actions directly using store.dispatch

<button onClick={() = > store.dispatch({type: 'ADD'})}>+</button>
<button onClick={()= > store.dispatch({type: 'MINUS'})}>-</button>
Copy the code

The drawback is that redux provides the bindActionCreators function, in which store.dispatch is repeated many times, and action.type is easy to miswrite and difficult to find. Bind the function that dispatches the action to store.dispatch

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '.. /store'

// The add function returns action, so it can be called actionCreator
function add() {
  return {type: 'ADD'}}function minus() {
  return {type: 'MINUS'}}const bindAdd = bindActionCreators(add, store.dispatch)
const bindMinus = bindActionCreators(minus, store.dispatch)

class Counter extends React.Component{
	// ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={bindAdd}>+</button>
      <button onClick={bindMinus}>-</button>
    </div>}}export default Counter
Copy the code

In fact, the bindActionCreators logic can be pulled out of the 👆 code into a separate file that can be used in other components. Meanwhile, the above code does not make sense because it requires manual binding for each function, so bindActionCreators supports passing in objects and wrapping all actionCreator functions as objects

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '.. /store'

// The add function returns action, so it can be called actionCreator
function add() {
  return {type: 'ADD'}}function minus() {
  return {type: 'MINUS'}}const actions = {add, minus}

const bindActions = bindActionCreators(actions, store.dispatch)

class Counter extends React.Component{
  // ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={ bindActions.add} >+</button>
      <button onClick={ bindActions.minus} >-</button>
    </div>}}export default Counter
Copy the code

1.4.2 Handwriting implementation

function bindActionCreators (actionCreater, dispatch) {
  ActionCreater can be a function/object
  if (typeof actionCreater === 'function') {
  	return function (. args) {
    	returndispatch(actionCreater(... args)) } }else {
  	let bindActionCreaters = {}
    Object.keys(actionCreater).forEach(key= > {
    	bindActionCreaters[key] = function (. args) {
        returndispatch(actionCreater(... args)) } })return bindActionCreaters
  }
}

export default bindActionCreaters
Copy the code

1.5 combineReducers

1.5.1 Principle and Usage

When an application contains multiple modules, it is not reasonable to put the state of all modules on the reducer. It is better to divide each module by reducer, and each module has its own reducer and action. Finally, the reducer is merged into a large reducer through combineReducers in Redux

// src\store\reducers\index.js
import {combineReducers} from 'redux';
import counter1 from './counterReducer1';
import counter2 from './counterReducer2';
export default combineReducers({
    x: counter1,
    y: counter2
});

// src/store/reducers/counterReducer1.js
import * as types from '.. /action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD1:
            return state.count + 1;
        case types.MINUS1:
            return state.count - 1;
        default:
            returnstate; }}// src/store/reducers/counterReducer2.js
import * as types from '.. /action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD2:
            return state.count + 1;
        case types.MINUS2:
            return state.count - 1;
        default:
            returnstate; }}Copy the code

The combineReducers method accepts an object, and the property key can be set arbitrarily. The property value corresponds to the Reducer function of each module and returns a final reducer method after merging.

After the reducer merge, the state tree in the store will also be divided according to modules

store.getState() 
{
  x: {count: 0}
  y: {count: 0}}Copy the code

Thus, in the component, the use of state needs to be modified to look like this

import store from '.. /store';
export default class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
          value: store.getState().x.count
        }
    }
  	/ /...
}
Copy the code

When action is distributed in the component, it will be passed to the function returned by combineReducers. In this function, the reducer of each module will be called to generate its own new state. Finally, after all the states are merged, Update the state in store

1.5.2 Handwriting implementation

function combineReducers(reducers){
  // Return the reducer function after the merge
  return function (state, action){
  	const nextState = {}
    Object.keys(reducers).forEach(key= > {
    	nextState[key] = reducers[key](state[key], action)
    })
    return nextState
  }
}
Copy the code

It can be seen that the reducer function of each module will execute actions that are mainly distributed

1.6 summary

When you use stores in React, you need to manually import store files and subscribe to store state changes. This is not appropriate

Second, the React – story

2.1 Principle and use

The React-Redux provides a Provider component. Through the Provider component, you can transfer stores to its children and grandchildren without importing each component manually

// ./src/index.js
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root'));Copy the code

In the descendant Counter1, you can use react-redux to provide a connect function to associate the store with the props of the Counter1 component

import React from 'react'
import { connect } from 'react-redux'
import action from '.. /store/actions/Counter1'

class Counter1 extends React.Component{
  render () {
    return <div>
      <p>{ this.props.count }</p>
      <button onClick={ this.props.add} >+</button>
      <button onClick={ this.props.minus} >-</button>
    </div>}}const mapStateToProps = state= > state
constmapDispatchToProps = { ... action }export default connect(mapStateToProps, mapDispatchToProps)(Counter1)
Copy the code

As can be seen from the above code, properties or methods of Counter1 are accessed through props. It is completely possible to convert Counter1 into function components (stateless components), which are wrapped by a container component (stateful components). All connect(mapStateToProps, mapDispatchToProps)(Counter1) returns a container component. How to write a React-redux

2.2 Handwriting Implementation

To pass stores across components, react-Redux uses the React Context API internally

Create a ReactReduxContext context object

// src/react-redux/Context.js

import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext
Copy the code

In the Proveider component, you need to use the Provider component provided in the ReactReduxContext object

// src/react-redux/Provider.js
import React from 'react'
import {ReactReduxContext} from './Context'

class Provider extends React.Component{
  constructor(props) {
  	super(props)
  }
  render () {
  	return <ReactReduxContext.Provider value={{ store: this.props.store}} >
      {this.props.children}
    </ReactReduxContext.Provider>}}export default Provider
Copy the code

The connect method, which takes mapStateToProps and mapDispatchToProps, returns a function that takes a custom component (such as Counter1) and returns the final container component

// src/react-redux/connect.js
import React from 'react'
import {bindActionCreators} from 'redux'
import {ReactReduxContext} from './Context'

function connect(mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    // Returns the final container component
     return class extends React.Component{
    	static contextType = ReactReduxContext
    	constructor(props, context){
      	  super(props)
          this.state = mapStateToProps(context.store.getState())
        }
    	shouldComponentUpdate() {
          if (this.state === mapStateToProps(this.context.store.getState())) {
            return false;
          }
          return true;
        }
    	componentDidMount () {
      	  this.unsubscribe = this.context.subscribe(() = > {
        	this.setState(mapStateToProps(this.context.store.getState()))
          })
        }
    	componentWillUnmount (){
      	  this.unsubscribe && this.unsubscribe()
        }
        render(){
          const actions = bindActionCreators(
            mapDispatchToProps,
            this.context.store.dispatch
          )
      	  return <WrappedComponent {. this.state} {. this.props} {. actions} / >}}}}export default connect
Copy the code

As you can see, in the Connect method, bindActionCreators bundled the Action with store.dispatch, and state changes in the subscription Store, which we did with redux only. The React component needs to be written manually. Fortunately, react-Redux now does that for us

Third, summary

Why redux and React-Redux should be introduced in react applications