preface

React, React-Redux, and the simplest implementation of the core API will be discussed in this article. Suitable for readers who have already used Redux but are not familiar with the principles.

Introduction to the

Before we get started, let’s take a quick look at the two libraries and their differences

Redux

Redux is a Javascript state management library. What he did was very simple: an object was used to describe the state of the application, and the state could only be modified by actions and reducer. It also provides a subscription service that notifies all subscribers when their status changes.

  • state tree

    A state tree that stores application states.

  • {type: ‘ADD’,payload: 1}. The type field tells reducer how to change the state in the store. Payload is the data carried.

  • Reducer A pure function that returns a new state. It receives an action and returns a new state tree by making different operations on the global state based on the Type field.

The core part of Redux is solely responsible for state maintenance and subscription notifications and is completely framework independent.

React-Redux

React Redux is the React Redux UI binding library, which allows users to retrieve Redux states stored in components and update them.

Realize the story

The basic use

Let’s take a look at this DEMO and see how to use Redux, okay

import { createStore } from 'redux'
const preloadedState = {
  count: 0};// reducer
const reducer = (state, action) = > {
  switch (action.type) {
    case "INCREASE":
      return { ...state, count: state.count + 1 };
    case "DECREASE":
      return { ...state, count: state.count - 1 };
    case "SET_COUNT":
      return { ...state, count: action.payload };
    default:
      returnstate; }};// action
const actions = {
  increment: () = > ({ type: "INCREASE" }),
  decrement: () = > ({ type: "DECREASE" }),
  setCount: (count) = > ({ type: "SET_COUNT".payload: count }),
};
const store = createStore(reducer, preloadedState);

const state = store.getState() // Get the state tree
store.subscribe(() = > console.log('store change!,store.getState())); // The subscription status changes
store.dispatch(actions.setCount(233)); // Trigger a state change
store.dispatch(actions.increment()); // Trigger a state change
Copy the code

You can see that Redux contains the following methods

  • createStore

Receive a default state tree and a Reducer function and return a store containing the following methods

  • Store. getState Obtains the status
  • Store. dispatch Changes the status by sending action
  • Store. Subscribe Changes the subscription status

createStore

getState

GetState is very simple, we just return the state defined in the function.

function createStore(reducer, preloadedState) {
  let state = preloadedState;  // Define the initial state tree
  
  const getState = () = > state; // Return the status tree
  
  return { getState };
}
Copy the code

dispach

This is a function to modify the state. As we know from the previous introduction, state should not be directly modified, but a new state tree should be obtained by action and Reducer every time update is needed.

const newState = reducer(oldState,action)
Copy the code

Therefore, what Dispatch needs to do is to receive an action and generate a new state and replace the old state through the reducer

function createStore(reducer, preloadedState) {
 let state = preloadedState;  // Define the initial state tree
 
 const getState = () = > state; // Return the status tree
 
 const dispatch = (action) = > {
   // Get a new state tree
   state = reducer(state, action);
   
   // All subscribers also need to be notified here
 };
 return { getState, dispatch };
}
Copy the code

subscribe

The creation, retrieval, and modification of states we have implemented are only operations on variables inside closures and are not perceived by the outside world. Subscribe needs to provide a function that notifies external subscribers of status updates.

Here we can achieve this by implementing a simple publish/subscribe model. Maintain an array of listeners inside the function that holds the subscriber’s callbacks. Every time the state changes (dispatch is invoked), all callbacks are executed.

 function createStore(reducer, preloadedState) {
 let state = preloadedState;  // Define the initial state tree
 
 const getState = () = > state; // Return the status tree
 
 const listeners =  [];  
 / / subscribe
 const subscribe = (fn) = > {
   listeners.push(fn);
   // Unsubscribe
   return () = > {
     const index = listeners.find((item) = > item === fn);
     listeners.splice(index, 1);
   };
 };
 const dispatch = (action) = > {
   // Get a new state tree
   state = reducer(state, action);
   // Notify all subscribers
   listeners.forEach((fn) = > fn());
 };
 return { getState , dispatch, subscribe };
}
Copy the code

Now that we’ve implemented a beggar version of Redux, let’s test it out

const preloadedState = {/ * *... * /};
const reducer = (state, action) = > {/ * *... * /};
const actions = {/ * *... * /};

const store = createStore(reducer, preloadedState);
console.log('Initial state' , store.getState())

const unsubscribe = store.subscribe( / / subscribe
   () = > console.log('I know state has changed.',store.getState())
); 
store.dispatch(actions.setCount(233)); 
store.dispatch(actions.increment());

unsubscribe(); // Unsubscribe
store.dispatch(actions.increment());  // I will not be notified of this update
Copy the code

combineReducers

As the application becomes more complex, we can consider splitting the Reducer function into separate functions, each responsible for independently managing a portion of state.

Redux has several extension apis to enhance reducer and dispatch. CombineReducers can combine multiple reducer functions into a final reducer function. ApplyMiddleware allows us to wrap our own Store dispatches to enhance functionality, such as asynchronous actions.

This section only introduces combineReducers, which receives an object and controls the name of the returned state key by naming different keys for the reducer of the incoming object.

// Reducer after the merge
const rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// Accordingly, the structure of state must be
const rootState = { potato: {}, tomato: {}}const store = createStore(rootReducer, rootState)
Copy the code

The idea is to disassociate reducer and state with some states through the key of the incoming object. When the reducer is received, all the new states need to be obtained by executing the Reducer, and then reassemble these states into a new state tree.

export default function combineReducers(reducers) {
    const reducerKeys = Object.keys(reducers)
    // Return the merged reducer function
    return function combinedReducer(state = {}, action) {
      / / the new state
      const nextState = {}
      // Iterate through all the reducers
      for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]   // State key
        const reducer = reducers[key] // Reducer corresponding to key
        // The current key corresponds to the old value of state
        const prevKeyState = state[key]
        // Run the reducer command to obtain the new state value corresponding to the current key
        const nextKeyState = reducer(prevKeyState, action)
        // Assemble the final state
        nextState[key] = nextKeyState
      }
      return nextState
    }
  }
Copy the code

Implement the React – story

The basic use

Just like before, let’s do a DEMO

import store from './store'
import { Provider,connect } from 'react-redux'
function App() {
  return (
    <Provider store={store}>
      <Header />
      <Main />
      <Footer />
    </Provider>)}// Map state to props
const mapState = (state) = > ({ count: state.count });
// Map dispatch to props
const mapDispatch = (dispatch) = > ({
  increment: () = > dispatch({ type: "increment"})});const Header = connect(mapState)((props) = > {
  const { count } = props;
  return <header> header: {count}</header>;
});
const Main = connect(mapState,mapDispatch)((props) = > {
  const { count, increment } = props;
  return (
    <main>
      Main: count:{count} <button onClick={increment}>increase </button>
    </main>
  );
});
function Footer(props) {
  return <footer>Footer does not use state</footer>;
}

Copy the code

As you can see, React-Redux provides a Provider component that transparently passes stores created in redux to all child components.

And a connect function that allows wrapped components to access the ‘mapped’ state and dispatch functions in the store directly through props. The map here can be understood as a mapping.

  • mapStateToPropsThe function gives you controlstoreWhich states are mapped to the props of the component.
  • mapDispatchToPropsSo you can encapsulate it yourselfdispatchDelta function and map to deltapropsIn the.

By default, the component gets the entire Store and Dispatch functions if none of them are passed.

connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Copy the code

Context

Use the React createContext method to create a context directly

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

Provider

The Provider receives the Store and passes it to all its children through the ReduxContext

import React from 'react'
import ReduxContext from "./Context";
const Provider = (props: any) = > {
  const{ store, children, ... rest } = props;// Pass store transparently to all child components
  return (
    <ReduxContext.Provider value={store} {. rest} >
      {children}
    </ReduxContext.Provider>
  );
};
export default Provider;
Copy the code

connect

Let’s infer the structure of the CONNECT function from how connect is called.

connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Copy the code

First of all, it receives mapStateToProps, mapDispatchToProps these two functions, and returned to a function, the function receives a component, and can make props for parameters of the component.

function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(component) {
   // Here you need to inject parameters into the props of the component
  };
}
Copy the code

How do I get the store in the wrapWithConnect function and pass it to the props of the component? Store is transparently transmitted through context, and context can only be consumed in a component, so it is obvious that this functionality needs to be implemented through high-level component (HOC).

function connect(mapStateToProps? : any, mapDispatchToProps? : any) {
  return function wrapWithConnect(component) {
    // Wrap the component, mainly to get the component context
    const HOC = (props) = > {
      const { dispatch, getState, subscribe } = useContext(AppContext);
      const state = getState();
      function childPropsSelector(state) {
        // State injected into props
        // It could be the entire store, or it could be a few specific states returned by mapState
        const stateProps = mapStateToProps ? mapStateToProps(state) : state;
        // A function injected into props to change state
        // It can be the original dispatch, or it can be a specific status update function returned by mapDispatch
        const dispatchProps = mapDispatchToProps
          ? mapDispatchToProps(dispatch)
          : dispatch;
        return{... stateProps, ... dispatchProps, ... props }; }// Get props for the final child component
      const actualChildProps = childPropsSelector(state);
      return React.createElement(component, actualChildProps, props.children);
    };
    return HOC;
  };
}

Copy the code

By wrapping a tier one component, we can take store information from the context and pass it on to the wrapped component.

Next we need to implement component updates. In React, components are updated using setState, which triggers updates every time a new value is passed in.

With the subscribe method provided by the Store, we know when the state has been dispatched and changed.

  // Force a component update
  const [, forceUpdate] = useState({});
  useEffect(() = > {
    const unsubscribe = subscribe(() = > {
      forceUpdate({});
    });
    returnunsubscribe; } []);Copy the code

There’s a little optimization that needs to be done here. The current code will receive notification in the component and trigger the render of the component even if the post-dispatch state has not changed.

// Record the state of the last render
const preStateRef = useRef({});
preStateRef.current = state

/ / subscribe to store
useEffect(() = > {
const unsubscribe = subscribe(() = > {
  // Compare the new state with the old state
  const state = getState();
  if (!isShadowEqual(preStateRef.current, state)) {
    forceUpdate({});
  }
});
returnunsubscribe; } []);Copy the code

Using a ref to record the old state and a shallow comparison of the old and new states before each render can reduce unnecessary rerendering. Of course, the real scenario is not only such a simple judgment, check the source code react-redux source code

The final implementation code is as follows:

import React, { useContext, useEffect, useRef, useState } from "react";
import ReduxContext from "./Context";

function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(component) {
    const HOC = (props) = > {
      const { dispatch, getState, subscribe } = useContext(ReduxContext);
      const state = getState();
      const [, forceUpdate] = useState({});
      function childPropsSelector(state) {
        const stateProps = mapStateToProps ? mapStateToProps(state) : state;
        const dispatchProps = mapDispatchToProps
          ? mapDispatchToProps(dispatch)
          : dispatch;
        return{... stateProps, ... dispatchProps, ... props }; }// Props for the final subcomponent
      const actualChildProps = childPropsSelector(state);

      // Record the state of the last render
      const preStateRef = useRef({});
      preStateRef.current = state;

      / / subscribe to store
      useEffect(() = > {
        const unsubscribe = subscribe(() = > {
          // Compare the new state with the old state
          const state = getState();
          if (!isShadowEqual(preStateRef.current, state)) {
            forceUpdate({});
          }
        });
        returnunsubscribe; } []);return React.createElement(component, actualChildProps, props.children);
    };
    return HOC;
  };
}

function isShadowEqual(origin: any, next: any) {
  if (Object.is(origin, next)) {
    return true;
  }
  if (
    origin &&
    typeof origin === "object" &&
    next &&
    typeof next === "object"
  ) {
    if (
      [...Object.keys(origin), ...Object.keys(next)].every(
        (k) = >
          origin[k] === next[k] &&
          origin.hasOwnProperty(k) &&
          next.hasOwnProperty(k)
      )
    ) {
      return true; }}return false;
}
export default connect;
Copy the code

Hooks API

In addition to using connect, react-Redux also supports hook access to stores

  • useStoreGet the store object
  • useSelectorUse the selector function fromstateExtract data from
  • useDipatchGet the Dispatch function
import React from "react";
import { useSelector, useStore, useDispatch } from "react-redux";
const CounterComponent = () = > {
  const store = useStore()
  const count = useSelector((state) = > state.counter);
  const dispatch = useDispatch()
  return (
      <div>
          {count}
          <button onClick={()= > dispatch({type: 'inc'})}>increase</button>
      </div>
  );
};
Copy the code

To access the store, we need to get the context, and none of these calls need to pass in the context ourselves, so we need to get the context object first.

import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
  const useSelector = (selector) = > {}
  return useSelector
}
export default createSelectorHook(context);
Copy the code

Once you have the context, you can access the store via React. UseContext, and then do the same with connect

import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
  const useReduxContext = () = > React.useContext(context)
  const useSelector = (selector) = > {
    const { getState,subscribe } = useReduxContext()
    const state = getState()
    const selectedState = selector(state);
     // Force a component update
     const [, forceUpdate] = useState({});
      / / subscribe to store
      useEffect(() = > {
        const unsubscribe = subscribe(() = > {
          // A shallow comparison of state omitted here
          forceUpdate({});
        });
        returnunsubscribe; } []);return selectedState;
  }
  return useSelector
}
export default createSelectorHook(context);
Copy the code

The other two apis have similar ideas, which I won’t repeat here. Hooks are more convenient to use in function components without having to provide a layer to the component package. Connect is more suitable for class components, and the @Connect decorator can simplify the process of wrapping this layer.

conclusion

In this paper, some common functions of React and React-redux are implemented by hand through some code examples. The implementation refers to the source code but simplifies many steps of optimization and wrong judgment. The main point is to sort out the general idea of implementing the function. If there is any incorrect description, welcome to correct!