1. Understanding of Redux

1.1 Why Redux?

  • JavaScript applications have become increasingly complex
    • Javascript has more and more complex states to manage
    • These states include data returned by the server, cached data, and data generated by user actions
  • Managing changing states is difficult:
    • States depend on each other. A change in one state will cause a change in another state, and a View page may also cause a corresponding change
    • When the application is complex, state changes can become difficult to control and track
  • React helps us deal with the DOM rendering process at the view level, but State is still left to our own management
    • Either the props of the component itself or the communication between components is passed through the props; It also includes sharing between contexts
    • React mainly helps us manage our views. How do we maintain state is ultimately up to us

U I = r e n d e r ( s t a t e ) UI = render(state)
  • Redux is a container that helps us manage state:

1.2 What is Redux?

  • Redux is a state container for JavaScript applications that provides predictable state management.
  • This allows you to develop stable and predictable applications that run in different environments (client, server, native application) and are easy to test.
  • Redux also supports other interface libraries in addition to React. It is small and compact (only 2kB, including dependencies), but has a strong plugin extension ecosystem.

1.3 Core concept of Redux

  1. Store: Stores data
{
  todos: [{
    text: 'Eat food'.completed: true
  }, {
    text: 'Exercise'.completed: false}].visibilityFilter: 'SHOW_COMPLETED'
}
Copy the code
  1. Action: Redux requires that updated data be modified via dispatch actions, which are plain JavaScript objects that describe what happened. The advantage of forcing action to describe all changes is that it is clear what is happening in the application. If something changes, you can see why. Actions are like indicators of what’s going on.
{ type: 'ADD_TODO'.text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO'.index: 1 }
{ type: 'SET_VISIBILITY_FILTER'.filter: 'SHOW_ALL' }
Copy the code
  1. Reducer: String actions and states together. Reducer is a pure function that receives state and action and returns the new state
// Reducer1 accepts state and Action and returns the new state
function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

// Reducer1 accepts state and Action and returns the new state
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) = >
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

// Develop a reducer call to manage the state of the entire application:
function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}
Copy the code

1.3 The Three Principles of Redux

  1. Single data source
  2. State is read-only
  3. Use pure functions to perform modifications

2. Basic use of Redux

store.js

import { createStore } from 'redux'
// const redux = require('redux')

/ / state
const initState = {
  value: 0
}

/** * The reducer function connects store and action@param {*} State The root state value is usually an object *@param {*} Action Describes the "what happened" action object *@returns  Returns a new status value */
function counterReducer(state = initState, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 } // Do not change the state object, but return a new object when the state changes
    case 'counter/decremented':
      return { value: state.value - 1 } // Do not change the state object, but return a new object when the state changes
    default:
      return state
  }
}

// Create a store to store state
let store = createStore(counterReducer)

// The UI can be updated with subscribe in response to state changes
// Usually use view binding libraries (e.g. React Redux) instead of using subscribe() directly
store.subscribe(() = > console.log('The state has changed', store.getState()))

// The only way to change the internal state is to dispatch an action.
store.dispatch({ type: 'counter/incremented' })
store.dispatch({ type: 'counter/incremented' })
store.dispatch({ type: 'counter/decremented' })
Copy the code

3. Redux integrates React

App.js

import React, { PureComponent } from 'react'
import store from './App'

export default class App extends PureComponent {
  
  constructor(props) {
    super(props)
    this.state = {
      // Store API :getState returns the current state
      counterState: store.getState()
    }
  }
  
  componentDidMount() {
    // Store API: Subscribe listens for state changes and updates the UI in response to state changes
    store.subscribe(() = > {
      console.log(store.getState());
      this.unsubscribe = this.setState({
        counterState: store.getState()
      })
    })
  }

  componentWillUnmount() {
    // Unsubscribe to listen for state changes
    this.unsubscribe()
  }

  render() {
    return (
      <>
        <div>counter: { this.state.counterState.value }</div>
        <button onClick={ e= > store.dispatch({ type: 'counter/decremented' })}>-1</button> 
        <button onClick={ e= > store.dispatch({ type: 'counter/incremented' })}>+1</button> 
      </>)}}Copy the code

4. React-Redux

So far we’ve actually mastered the basics of Redux and how to combine it with React, but the current code isn’t very elegant.

Since the above logic is used every time a component is written, is it possible to try to pull out the repetitive logic

4.1 Customizing connect functions

connect.js

import React, { PureComponent } from 'react'
import store from './App'

const connect = function(mapStateToProps,mapDispatchToProps) {
  return function(WrapperComponent) {
    return class extends PureComponent {
      constructor(props) {
        super(props)
        this.state = {
          counterState: mapStateToProps(store.getState())
        }
      }

      componentDidMount() {
        this.unsubscribe = store.subscribe(() = > {
          this.setState({
            counterState: store.getState()
          }, () = > {
            console.log('State has changed :'.this.state.counterState.value); })})}componentWillUnmount() {
        this.unsubscribe()
      }

      render() {
        return (
          <WrapperComponent 
            {. this.props} 
            {. mapStateToProps(store.getState()} {. mapDispatchToProps(store.dispatch)} / >)}}}}export defalut connect
Copy the code

App.js

import connect from './connect.js'

function App(props) {
  return (
    <>
      <div>counter: { props.counterState.value }</div>
      <button onClick={ e= > props.decremented()}>-1</button> 
      <button onClick={ e= > props.incremented()}>+1</button> 
    </>)}// Do not directly assign an object to as few stores as possible
// const mapStateToProps = {
// counterState: store.getState()
// }

// Do not assign an object to as few stores as possible
// const mapDispatchToProps = {
// incremented() {
// store.dispatch({ type: 'counter/incremented' })
/ /},
// decremented() {
// store.dispatch({ type: 'counter/decremented' })
/ /}
// }

const mapStateToProps = state= > {
  return {
    counterState: state
  }
}

const mapDispatchToProps = dispatch= > {
  return {
    incremented() {
      dispatch({ type: 'counter/incremented'})},decremented() {
      dispatch({ type: 'counter/decremented'})}}}export default connect(mapStateToProps, mapDispatchToProps)(App)
Copy the code
  • But the connect function above has one big flaw: it relies on an imported store
  • We want the currently wrapped function to have no store dependency. Instead, we provide a Provider that comes from the Context we created and lets the user pass the store into the value

4.2 Context Processing store

context.js

import { createContext } from "react";

const StoreContext = createContext()

export default StoreContext
Copy the code

index.js

import App from './App.js'
import SotreContext from './context.js'
import store from './store.js'

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

connect.js

import React, { PureComponent } from 'react'
import store from './store.js'
import StoreContext  from './context.js'

const connect = function(mapStateToProps,mapDispatchToProps) {
  return function(WrapperComponent) {
    return class extends PureComponent {
      The static keyword is equivalent to adding a class attribute contextType and assigning it to StoreContext
      static contextType = StoreContext
      constructor(props, context) {
        super(props)
        this.state = {
          // context replaces store
          counterState: mapStateToProps(context.getState())
        }
      }

      componentDidMount() {
        // context replaces store
        this.unsubscribe = this.context.subscribe(() = > {
          this.setState({
            counterState: this.context.getState()
          }, () = > {
            console.log('State has changed :'.this.state.counterState.value); })})}componentWillUnmount() {
        this.unsubscribe()
      }

      render() {
        return (
          // context replaces store
          <WrapperComponent 
            {. this.props} 
            {. mapStateToProps(this.context.getState()} {. mapDispatchToProps(this.context.dispatch)} / >)}}}}Copy the code

4.3 Use of React-Redux

React-redux is an official view-binding library that lets your React component read data from the Redux store and dispatch operations to the store to update state.

At this point, using react-redux is actually quite simple, requiring only minor changes to the 4.2 code

index.js

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

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

App.js

import React, { PureComponent } from 'react'
import store from './store.js'

import { connect } from 'react-redux'

function App(props) {
  return (
    <>
      <div>counter: { props.counterState.value }</div>
      <button onClick={ e= > props.decremented()}>-1</button> 
      <button onClick={ e= > props.incremented()}>+1</button> 
    </>)}const mapStateToProps = state= > {
  return {
    counterState: state
  }
}

const mapDispatchToProps = dispatch= > {
  return {
    incremented() {
      dispatch({ type: 'counter/incremented'})},decremented() {
      dispatch({ type: 'counter/decremented'})}}}export default connect(mapStateToProps, mapDispatchToProps)(App)

Copy the code

Redux middleware

5.1 Introduction to Redux Middleware

  • The problem

    The state storage mentioned above is the general local state, and the distributed action is also synchronous code, and ReDUx does not manage the state of the asynchronous request data by default, but in fact, the network request data is also part of our state management, in fact, should also be handed over to Redux to manage. So how should redux operate asynchronously? The answer is middleware.

  • Redux middleware concepts

    • The purpose of this middleware is to extend some of its own code between the actions of the Dispatch and the final reducer
    • Such as logging, calling asynchronous interfaces, and adding code debugging capabilities

5.2 Middleware – Redux-thunk

  • How is redux-Thunk capable of sending asynchronous requests?
    • By default, the Dispatch action is an object
    • Redux-thunk allows action to be a function
    • This function is going to be called, and it’s going to have a dispatch function and a getState function
      • Dispatch Dispatches actions
      • The getState function takes into account the original state that some operations after that require, and allows us to retrieve some of the previous states
  • The use of story – thunk

Line 4 below adds support for the browser redux-devTools plug-in

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
  applyMiddleware(thunkMiddleware) // Add redux-thunk middleware
));
Copy the code
// Before dispatch accepts an object
const mapDispatchToProps = dispatch= > {
  return {
    incremented() {
      dispatch({ type: 'counter/incremented'})},decremented() {
      dispatch({ type: 'counter/decremented'})}}}// Accept a function with redux-thunk dispatch
const mapDispatchToProps = dispatch= > {
  return {
    incremented() {
      dispatch(asyncAction)
    },
    decremented() {
      dispatch({ type: 'counter/decremented'})}}}const asyncAction = (dispatch, getStatae) = > {
  // Simulate an asynchronous request
  setTimout(() = > {
    dispatch({ type: 'counter/incremented' })
    dispatch({ type: 'counter/incremented' })
    dispatch({ type: 'counter/incremented'})},3000)}Copy the code

5.3 Middleware ii Redux-Saga

import { createStore, applyMiddleware, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'
import saga from './saga'

const initState = {
  value: 0
}


const reducer = (state = initState, action) = > {
  switch(action.type) {
    case 'increment':
      return { ...state, value: state.value + 1 }
    case 'decrement':
      return { ...state, value: state.value - 1 }
    default:
      return state
  }
}

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// Create sagaMiddleware middleware
CreateSagaMiddleware is a function that is different from thunkMiddleware
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
  applyMiddleware(sagaMiddleware)
));

// For sagaMiddleware to work, the run method is called. The run method receives a generator function that describes which actions to intercept and which actions to take
sagaMiddleware.run(saga)

export default store
Copy the code

saga.js

import { takeEvery,takeLatest, put, all } from 'redux-saga/effects'

const decrementSaga = function* (){
    // TODO: simulate TODO asynchronous request interface data
    yield new Promise((resolve, reject) = > {
      setTimeout(() = > {
        resolve('wake up')},3000);
    })
  
    all([
      yield put({type: 'decrement'})
      yield put({type: 'decrement'})
      yield put({type: 'decrement'}})])function* saga() {
  / / can use takeEvery | takeLatest receives two parameters the first parameter is the type of action the second parameter is a function generator
  // The difference is takeLatest: only one action can be listened on at a time, and if multiple actions are dispatched, it will execute only one last time
  // takeEvery: will be executed every time
  Call action='decrementSaga'. DecrementSaga is a generator function
  yield takeEvery('decrementSaga' , decrementSaga)
}

// Returns a generator function
export default saga
Copy the code

Component invocation

const mapDispatchToProps = dispatch= > {
  return {
    // Here dispatch still accepts a function
    decrementSaga() {
      dispatch({ type: 'decrementSaga'})}}}Copy the code