What is the story

Simply put, Redux is a state management container that provides patterns and tools to make it easier to trace state changes.

Unidirectional data flow

As you can see, the one-way data flow in the figure consists of the following parts:

  1. View: The View layer, which can be seen as a component in React
  2. Action: A message sent when an Action is performed in the View
  3. Dispatch: sender that receives an Action and informs the Store to change data based on the Action
  4. Store: Used to Store application status. When the status changes, it notifies the View to update it

When the user accesses the View, the user goes through the following steps:

  1. The user accesses a View, which takes the state from the Store to render the page
  2. When the user performs an Action on the View, such as clicking, an Action is generated for the Action
  3. After the Action is generated, it is sent to the Store via Dispatch for status updates
  4. When the Store update is complete, the View is notified to use the latest status update page

In the above process, the flow of data is always one-way, and there is no “two-way flow” of data in the adjacent parts.

Data processing in REDUx also uses the concept of one-way data flow.

The composition of the story

Redux consists of six parts:

  1. Store: Saves the status of an application
  2. State: indicates the status of the application
  3. Action: Have onetypeProperty, representing changesstateThe intent of the
  4. Dispatch: Receives oneactionSent to thestore
  5. Reducer: Reducer is a pure function that receives the last onestateandaction, through the last onestateandactionThe data is recalculated to newstateMake a return update.
  6. Middleware: Middleware that uses high-order function pairsdispatchReturns an enhanceddispatchfunction

So how do they work together?

  • For the first time to start
    • Create a store for the parameters using the root Reducer function
    • Store makes a call to the root Reducer and saves the value it returns as the initial state
    • When the UI is first rendered, the UI component takes the state out of the Store and renders the interface based on the state. The Store is also listened on to see if the state has changed, and if so, the interface is updated based on the new state.
  • update
    • When the user interacts with the UI, for example, click events
    • Dispatch An action to a store, e.g. Dispatch ({type: “increment”})
    • The Store runs the reducer again with the previous state and the current action, saving the return value as the new state
    • The Store notifies all subscribed UIs of the new state change
    • When the subscribed UI is notified, the state update interface is retrieved from the Store

To quote the official flow GIF:

Implement a REdux

Now that we have a rough idea of what reDUx consists of and how it works, let’s implement a ReDUx.

Create the store

Before creating the Store, let’s analyze what it does:

  1. Storage condition
  2. Changes in state that need to be listened on, and UI components need to be notified of state updates (subscriber mode)
  3. Return four methods:
    • Dispatch (Action) : dispatches actions to stores
    • GetState: Gets the state of the current store
    • Subscribe (listener) : Registers a callback function when state changes
    • ReplaceReducer (nextReducer) can be used for hot overloading and code splitting. Usually you don’t need to use this API (it won’t be implemented this time).

Now that you know what the Store does, let’s implement it.

// Create a new file: createstore.js
export default function createStore(reducer) {

    let currentState = null; // The state of the store
    let currentListeners = []; // Event center
    
    function dispatch(action) {
        // The only way to change state is to calculate the new state using the action and the previous state via reduce
        currentState = reducer(currentState, action);
        // When state is triggered, inform the view that the state update needs to be re-rendered
        currentListeners.forEach((listen) = > listen());
    }
    
    // Get the state of the current store
    function getState() {
        return currentState;
    }
    
    // Register a listener when state changes
    function subscribe(listen) {
        currentListeners.push(listen);
        
        // Returns a function to unlisten
        return function unsubscribe() {
            const index = currentListeners.indexOf(listen);
            currentListeners.splice(index, 1); }}// Obtain the initial value. In Redux, reduce is invoked in combineReduces to obtain the initial value
    dispatch({type: 'REDUX/XXX'});
    
    return {
        dispatch,
        getState,
        subscribe
    };
}
Copy the code

Now that the store implementation is complete, let’s write a demo to verify that:

// Create a reducer
function testReducer(state, action) {
    switch (action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
    default:
      return 0; }}/ / create a store
const {dispatch, getState, subscribe} = createStore(testReducer);

// Listen for state changes
const unsubscribe = subscribe(() = > {
    // Get the latest state
    const state = getState();
    console.log('new state:', state); // Print the result: 1
});

// Initiate an action to update state
dispatch({type: 'increment'});

// Remove the listener
unsubscribe();
Copy the code

After running the demo, the result is perfect.

Adding Middleware

As we know, Dispatch in Redux only accepts an object with a type attribute as an action. If we want to pass in functions, or request actions with other side effects such as interfaces, redux itself does not support this. Then middleware is needed to enhance the functionality of Dispatch to achieve the purpose of support.

Middleware structure:

function middleware({dispatch, getState}) {
    return (next) = > {
        return (action) = > {
            // This function can be seen as an enhanced dispatch
            returnnext(action); }}}Copy the code

As you can see, the middleware is actually a function. It takes the store Dispatch and getState apis as parameters and returns a function with next parameters. This internal function returns a function with Action as parameters and calls Next in this function.

What is Next? The next function refers to the next middleware, and if there is only one or the last middleware, its next is the dispatch function.

To implement adding middleware, let’s examine this function:

  1. Need to pass to middlewarestoreThe API:Dispatch, getState
  2. Multiple middleware enhancements need to be combineddispatch

Below, we implement adding middleware:

// Create an applyMiddleware. Js file
export default function applyMiddleware(. middlewares) {
    return (createStore) = > (reducer) = >{}; }Copy the code

We know that the applyMiddleware function in Redux is passed to the createStore method as an argument:

createStore(reducer, applyMiddleware(middleware1, middleware2));
Copy the code

So we need to modify createStore from the previous:

// createStore.js
export default function createStore(reducer, enhancer) {

    // When middleware is passed in, use middleware to enhance dispatch
    if(enhancer) {
        returnenhancer(createStore)(reducer); }...return {
        dispatch,
        getState,
        subscribe
    };
}
Copy the code

In createStore, we decide whether to add middleware. If so, we pass applyMiddleware and Reducer into createStore. After middleware enhancement, the STORE API is returned.

Back to applyMiddleware:

// Create an applyMiddleware. Js file
export default function applyMiddleware(. middlewares) {
    return (createStore) = > (reducer) = > {
        / / create a store
        const store = createStore(reducer);
        
        //增强dispatch
        // The first step is to call the middleware and pass in dispatch and getState
        const middlewareApi = {
          getState: store.getState,
          dispatch: (action, ... args) = >store.dispatch(action, ... args), };// Call the middleware function, passing in the STORE API
        // The middleware structure in middlewareChain: is: (next) => (action) =>...
        const middlewareChian = middlewares.map((middleware) = > middleware(middlewareApi));
        
        * function m1(next) => function m1A(action)... * function m1(next) => function m1A(action)... * function m2(next) => function m2A(action) ... * pass m2 as a parameter to M1 and dispatch as a parameter to M2: * m1(m2(dispatch)) becomes M1 (m2A) * next in M1 = m2A (next in m2 = dispatch) * when next is called in M1, m2A is executed, Calling next in m2A calls the store dispatch method * * Using the compose method (array.reduce) returns the following function: * (dispatch) => m1(m2(dispatch)) * */
        // Use composition functions to merge middleware into an enhanced dispatch function
        constdispatch = compose(... middlewareChain)(store.dispatch);// Returns the store API and the enhanced Dispatch
        return {
          ...store,
          dispatch,
        };
        
    };
}

/** * merge multiple functions into one function */
function compose(. funcs) {
  // There is no middleware
  if(funcs.length === 0) {
    return (arg) = > arg;
  }

  // Only one middleware
  if(funcs.length === 1) {
    return funcs[0];
  }

  // Multiple middleware cases
  return funcs.reduce((a, b) = > (. arg) = >a(b(... arg))); }Copy the code

The main challenge is to combine multiple middleware to enhance the Dispatch method, which is somewhat convoluted and requires careful thinking.

Ok, the addition of middleware to this point is complete.

Create the Redux-Logger middleware

The redux-Logger prints the state before the update, and the action and state after the update:

// Create a redux-logger.js file
export default function logger({dispatch, getState}) {
  return next= > action= > {
    const preState = getState(); // Get the state before the update
    console.log('=======logger=======================start');
    console.log('preState:', preState);
    console.log('action', action);
    const returnValue = next(action); // Dispatch action initiates the state update
    const nextState = getState(); // Get the updated state
    console.log('nextState:', nextState);
    console.log('=======logger=======================end');
    returnreturnValue; }}Copy the code

Let’s write a demo to verify that this works:

/ / create a store
const {dispatch, getState, subscribe} = createStore(testReducer, applyMiddleware(logger));

// Listen for state changes
// Console print:
// =======logger=======================start
/ / preState: 0
// action {type: 'increment'}
/ / nextState: 1
// =======logger=======================end


const unsubscribe = subscribe(() = > {
    // Get the latest state
    const state = getState();
    console.log('new state:', state); // Print the result: 1
});

// Initiate an action to update state
dispatch({type: 'increment'});

// Remove the listener
unsubscribe();
Copy the code

And it turned out perfectly.

At this point, a simple, complete redux implementation is complete.

Implement the react – story

Redux is a standalone state management container that can be used with any JS framework, such as Vue, React, Angular, etc.

To use redux in React, connect React to Redux using react-redux.

We only implement a few of the commonly used React-Redux apis:

  1. Provider: the Provider of a store that passes the STORE API across hierarchies
  2. Connect: Connects the React component to the Redux store
  3. UseSelector: The hook function that gets the specified state
  4. UseDispatch: Hook function of the dispatch method

To realize the Provider

Provider is a component that passes the Store API across hierarchies by assigning a store value to the value property.

// Create the provider.js file
import React from 'react';

export const ReduxContext = React.createContext();

export default function Provider({store, children}) {
    return (
        <ReduxContext.Provider value={store}>
          {children}
        </ReduxContext.Provider>
    );
}
Copy the code

We use the React Context Api to implement store delivery across hierarchies.

To realize the connect

Connect is a higher-order function. Let’s analyze the function of connect:

  1. Connect the React component to Redux’s Store to pass properties like state and Dispatch to the React component
  2. Listen for changes in state, which force the component to update and rerender according to the latest state.

Ok, now that we know what connect does, let’s examine its call form:

function connect(mapStateToProps, mapDispatchToProps, mergeProps, options)
Copy the code

As you can see, connect takes four parameters, and we only implement the first two:

  1. MapStateToProps:
mapStateToProps? :(state, ownProps?) = > Object
Copy the code

MapStateToProps is a function that takes the store’s state and the props(ownProps) passed by the parent component as arguments, and returns an object that is passed to the React component’s props.

  1. MapDispatchToProps:
mapDispatchToProps? :Object | (dispatch, ownProps?) = > Object
Copy the code

You can see that mapDispatchToProps can be an object or a function. When a function receives the Store dispatch method as the first argument, and the parent component passes props(ownProps) as the second argument, the returned object is passed to the React component props:

const mapDispatchToProps = (dispatch, ownProps) = > {
    return {
        add: dispatch({type: 'add'});
    };
}
Copy the code

When passing an object, this object is passed to the React component props:

mapDispatchToProps = {
    add: () = > ({type: 'add'})};Copy the code

Let’s make it happen:

// Create a new connect.js file
import {useContext, useEffect, useReducer} from 'react';
import {ReduxContext} from "./provider";

// Wrap the Action object with dispatch and pass it to the component to call directly instead of calling the Dispatch method separately
function bindActionCreators(actionCreators, dispatch) {
  const boundActionCreators = {dispatch};

  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];
    boundActionCreators[key] = (. args) = >dispatch(actionCreator(... args)); }return boundActionCreators;
}

// Connect accepts two parameters: mapStateToProps and mapDispatchToProps
const connect = (mapStateToProps, mapDispatchToProps) = > {
    // Connect is a higher-order function that needs to return a function that takes a component as an argument and returns a new component
    const wrapWithConnect = WrappedComponent= > props= > {
        // Get the store API
        const {getState, subscribe, dispatch} = useContext(ReduxContext); 
        const [ignore, forceUpdate] = useReducer((preValue) = > preValue + 1.0);
        / / for the state
        const state = getState();
        / / call mapStateToProps
        const stateProps = mapStateToProps && mapStateToProps(state, props);
        let dispatchProps = {dispatch};
        
        // Determine the type of mapDispatchToProps, whether it is a function or an object
        if(typeof(mapDispatchToProps) === 'function') {
          dispatchProps = mapDispatchToProps(dispatch, props);
        }else if (mapDispatchToProps && typeof(mapDispatchToProps) === 'object') {
          dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
        }
        
        // Listen for state changes and call forceUpdate to force a refresh
        useEffect(function componentDidMount(){
          const unsubscribe = subscribe(() = > {
            forceUpdate();
          });

          return function componentWillUnmount()  {
            unsubscribe && unsubscribe();
          }
        }, [subscribe]);
        
        
        return <WrappedComponent {. props} {. stateProps} {. dispatchProps} / >}}Copy the code

At this point the connect implementation is complete.

Implement useSelector

UseSelector features:

  1. You need to pass in a callback function that takes state and returns the specified state.
  2. Listen for changes in state that require the component to force an update
const num = useSelector((state) = > state.num);
Copy the code

Let’s implement it:

/ / create useSelector. Js

import {useContext, useEffect, useReducer} from 'react';
import {ReduxContext} from './provider';

export default function useSelector(selector) {
  // Get the store API
  const {getState, subscribe} = useContext(ReduxContext);
  // Force an update
  const [ignore, forceUpdate] = useReducer((preValue) = > preValue + 1.0);

  // Listen for state changes
  useEffect(function componentDidMount() {
    const unsubscribe = subscribe(() = > {
      forceUpdate();
    });
   
    return function componentWillUnmount() {
      unsubscribe && unsubscribe();
    }
  }, [subscribe]);

  // Pass in state and call the callback function to return the state specified by the callback function
  return selector(getState());
}
Copy the code

At this point useSelector is also implemented.

Implement useDispatch

The useDispatch function is to obtain the dispatch method from the store and expose it to external call:

import {useContext} from 'react';
import {ReduxContext} from './provider';

export default function useDispatch() {
  const store = useContext(ReduxContext);

  return store.dispatch;
}
Copy the code

Here a simple react-Redux implementation is complete.

Click here for the full source code