Redux is a state machine with no UI, so it is usually used with a UI library. For example, Redux in React is used with the react-Redux library. This library binds Redux’s state machine to React’s UI rendering and automatically updates the page when you dispatch an action to change state. In this article, I’ll write my own react-Redux to start with the basics, then replace the official NPM library and keep it functional.
All the code for this article has been uploaded to GitHub, you can take it down to play:Github.com/dennis-jian…
Basic usage
Here is a simple example of a counter that runs like this:
To implement this functionality, we need to add the React-Redux library to our project and wrap the entire ReactApp root component with its Provider:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import store from './store'
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>.document.getElementById('root'));Copy the code
As you can see from the above code, we also provide a store parameter to the Provider. This parameter is the store generated by Redux’s createStore. We need to call this method and pass in the returned store:
import { createStore } from 'redux';
import reducer from './reducer';
let store = createStore(reducer);
export default store;
Copy the code
The createStore parameter is a reducer, so we need a reducer:
const initState = {
count: 0
};
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return{... state,count: state.count + 1};
case 'DECREMENT':
return{... state,count: state.count - 1};
case 'RESET':
return{... state,count: 0};
default:
returnstate; }}export default reducer;
Copy the code
Reduce here has an initial state with a count of zero, and it can handle three actions that correspond to three buttons on the UI that add, subtract, and reset counts in state. ConnectAPI: react-Redux: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI For example, our counter component needs the count state and the add, subtract, and reset three actions, and we connect them with connect like this:
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from './actions';
function Counter(props) {
const {
count,
incrementHandler,
decrementHandler,
resetHandler
} = props;
return (
<>
<h3>Count: {count}</h3>
<button onClick={incrementHandler}>Count + 1</button>
<button onClick={decrementHandler}>Count - 1</button>
<button onClick={resetHandler}>reset</button>
</>
);
}
const mapStateToProps = (state) = > {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) = > {
return {
incrementHandler: () = > dispatch(increment()),
decrementHandler: () = > dispatch(decrement()),
resetHandler: () = > dispatch(reset()),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
Copy the code
As you can see from the above code, connect is a higher-order function. Its first order takes two parameters, mapStateToProps and mapDispatchToProps, which are both functions. MapStateToProps can customize which states need to be connected to the current component. These custom states can be obtained within the component using props. The mapDispatchToProps method passes in the Dispatch function. We can customize methods that call dispatch to the Dispatch action to trigger state updates. These custom methods can also be obtained by the component props. The second order of connect takes a component, and we can assume that this function is used to inject the previously defined state and method into the component, while returning a new component to the external call, so connect is also a higher-order component.
Here we take a look at the apis we use, and these are the goals we’ll be writing later:
Provider: a component that wraps the root component to inject Redux’s store.
CreateStore: The core method Redux uses to create a store, which we wrote in another article.
Connect: Used to inject state and dispatch into the desired component, returning a new component that is actually a higher-order component.
So the core of react-Redux is actually two apis, both of which are components and have similar functions. They both inject parameters into components. Provider injects store into the root component, and connect injects state and Dispatch into required components.
Before writing it by hand, let’s consider why react-Redux is designed with these two apis. If there are no these apis, is Redux ok? Of course it can! The whole point of Redux is to store the state of the entire application, update the state with a Dispatch action, and then the UI updates automatically. Why don’t I just start with the root component and pass the store down at each level? When each child component needs to read the state, just use store.getState() and store. Dispatch when updating the state. However, if you write it this way, if there are many nested levels of sub-components, each level needs to be passed in manually, which can be ugly and tedious to develop, and if a new student forgets to pass in the store, there will be a series of errors. So it’s nice to have something that can inject a global store into the component tree without having to pass layers as props, and that thing is Provider! Also, if each component relied on Redux independently, React’s data flow would be broken, which we’ll talk about later.
The React of Context API
React actually provides an API for injecting variables globally. This is called the Context API. Suppose I now have a requirement to pass a text color configuration to all of our components. Our color configuration is on the top component. When this color changes, all of the components below will automatically apply this color. We can inject this configuration using the Context API:
Use the firstReact.createContext
Create a context
// We use a separate file to call createContext
// Because the return value is referenced in different places by the Provider and Consumer
import React from 'react';
const TestContext = React.createContext();
export default TestContext;
Copy the code
useContext.Provider
Wrap root component
Having created the context, if we want to pass variables to some components, we need to add testContext. Provider to their root component and pass the variable as a value parameter to TestContext.provider:
import TestContext from './TestContext';
const setting = {
color: '#d89151'
}
ReactDOM.render(
<TestContext.Provider value={setting}>
<App />
</TestContext.Provider>.document.getElementById('root'));Copy the code
useContext.Consumer
Receive parameters
We passed the argument in with context. Provider, so that any child component wrapped by context. Provider can get the variable, but we need to use the context. Consumer package to get the variable. For example, our previous Counter component can get this color by wrapping the JSX it returns with context.consumer:
// Be careful to introduce the same Context
import TestContext from './TestContext';
/ /... Omit n lines of code...
// The returned JSX is wrapped around context.consumer
// Notice that context. Consumer is a method that accesses the Context parameter
// The context is the setting passed by the Provider, and we can get the color variable
return (
<TestContext.Consumer>
{context =>
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>Count + 1</button>
<button onClick={decrementHandler}>Count - 1</button>
<button onClick={resetHandler}>reset</button>
</>
}
</TestContext.Consumer>
);
Copy the code
In the above code we passed a global configuration through the context, and we can see that the text color has changed:
useuseContext
Receive parameters
In addition to the context. Consumer function, the React function also uses the useContext hook to accept Context parameters.
const context = useContext(TestContext);
return (
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>Count + 1</button>
<button onClick={decrementHandler}>Count - 1</button>
<button onClick={resetHandler}>reset</button>
</>
);
Copy the code
So we can use the Context API to pass the Redux store. Now we can also guess that the React-Redux Provider actually wraps the context.Provider and passes the redux store argument. The connectHOC of React-Redux is essentially a wrapped Context.Consumer or useContext. We can implement react-Redux ourselves in this way.
handwrittenProvider
The Provider uses the context API, so we need to create a context file and export the required context:
// Context.js
import React from 'react';
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
Copy the code
This file is very simple, just create a new context and export it, see the corresponding source code here.
Then apply this context to our Provider component:
import React from 'react';
import ReactReduxContext from './Context';
function Provider(props) {
const {store, children} = props;
// This is the context to pass
const contextValue = { store };
// Returns the ReactReduxContext wrapped component, passing in contextValue
// We will leave children alone
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>)}Copy the code
The Provider component code is not difficult, just pass the store into the context, and render the children directly.
handwrittenconnect
The basic function
In fact, connect is the most difficult part of React-Redux. It has complex functions and many factors to consider. To understand it, we need to look at it layer by layer.
import React, { useContext } from 'react';
import ReactReduxContext from './Context';
// The first function accepts mapStateToProps and mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
// The second level function is a higher-order component that gets the context
// Then execute mapStateToProps and mapDispatchToProps
// Render the WrappedComponent with this result combined with the user's parameters as the final parameters
// WrappedComponent is our own component wrapped with ConNext
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
// Make a copy of props to wrapperProps
const { ...wrapperProps } = props;
// Get the value of context
const context = useContext(ReactReduxContext);
const { store } = context; // Deconstruct store
const state = store.getState(); / / to the state
// Execute mapStateToProps and mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
// Assemble the final props
const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);
/ / render WrappedComponent
return <WrappedComponent {. actualChildProps} ></WrappedComponent>
}
returnConnectFunction; }}export default connect;
Copy the code
Triggered update
Replacing the official React-Redux with the Provider and connect above actually rendered the page, but clicking the button didn’t react because we changed the state in the store via dispatch, but the change didn’t trigger an update to our component. As Redux mentioned earlier, we can use store. Subscribe to listen for changes in state and execute callbacks. The callback we need to register here is to check if the props we gave to the WrappedComponent have changed. Re-render ConnectFunction if it changes, so here we need to solve two problems:
- When we
state
Change when the check is finally givenConnectFunction
Is there any change in the parameters of- If this parameter changes, we need to re-render
ConnectFunction
Checking parameter Changes
To check for changes in parameters, we need to know the last render parameters and the local render parameters and compare them. To find out the parameters of the last render, we can use useRef directly in ConnectFunction to record the parameters of the last render:
// Record last render parameters
const lastChildProps = useRef();
useLayoutEffect(() = >{ lastChildProps.current = actualChildProps; } []);Copy the code
Note that lastChildProps. Current is assigned after the first render, and useLayoutEffect is required to ensure immediate synchronization after rendering.
Because we need to recalculate the actualChildProps to detect parameter changes, the logic of the calculation is the same, so we pull this calculation logic out into a separate method childPropsSelector:
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); / / to the state
// Execute mapStateToProps and mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
Copy the code
The react-redux uses shallowEqual, which is a shallow comparison, to compare only one layer. If you have several layers of structures returned by mapStateToProps, for example:
{
stateA: {
value: 1}}Copy the code
React-redux is designed for performance reasons. Deep comparisons, such as recursive comparisons, can waste performance, and circular references can cause dead loops. Using shallow comparisons requires users to follow this paradigm and not pass in layers, which is also stated in the official documentation. Let’s just copy a shallow comparison here:
// shallowEqual.js
function is(x, y) {
if (x === y) {
returnx ! = =0|| y ! = =0 || 1 / x === 1 / y
} else {
returnx ! == x && y ! == y } }export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeofobjA ! = ='object' ||
objA === null ||
typeofobjB ! = ='object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if(keysA.length ! == keysB.length)return false
for (let i = 0; i < keysA.length; i++) {
if(!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]]) ) {return false}}return true
}
Copy the code
Detect parameter changes in callback:
// Register a callback
store.subscribe(() = > {
const newChildProps = childPropsSelector(store, wrapperProps);
// If the parameter changes, record the new value to lastChildProps
// And forces the current component to update
if(! shallowEqual(newChildProps, lastChildProps.current)) { lastChildProps.current = newChildProps;// An API is required to force updates to the current component}});Copy the code
Forced to update
There’s more than one way to force an update to the current component. If you’re using a Class component, you can just go this.setstate ({}), which is what the old react-Redux did. React-redux uses useReducer or useStatehook as the main useReducer provided by React.
function storeStateUpdatesReducer(count) {
return count + 1;
}
/ / ConnectFunction inside
function ConnectFunction(props) {
/ /... N lines of code left out...
// Use useReducer to force updates
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0);
// Register a callback
store.subscribe(() = > {
const newChildProps = childPropsSelector(store, wrapperProps);
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
forceComponentUpdateDispatch();
}
});
/ /... N lines of code omitted...
}
Copy the code
Connect this code is mainly corresponding to the source code of the connectAdvanced class, the basic principle and structure are the same as ours, but he wrote more flexible, Supports user passing custom childPropsSelector and merging methods of stateProps, dispatchProps, and wrapperProps. Interested friends can go to see his source: github.com/reduxjs/rea…
React-redux is now available to replace the official react-redux with the counter function. But here’s how React-Redux ensures that components are updated in order, because a lot of the source code deals with this.
Ensure that components are updated in sequence
Our Counter component connects to the Redux store using connect. If there are other children connected to the Redux store, we need to consider the order in which their callbacks are executed. React is a one-way data flow, and its parameters are passed from parent to child. Redux is now introduced. Even though both parent and child components reference the same variable count, the child component can take the parameter directly from Redux instead of the parent component. In a parent -> child one-way data stream, if one of their common variables changes, the parent must update first, and then the parameter is passed to the child to update, but in Redux, the data becomes Redux -> parent, Redux -> child, and the parent and child can be updated independently based on the data in Redux. However, it cannot guarantee the process that the parent level updates first and the child level updates again. React-redux ensures this update order manually by creating a listener class outside the Redux store Subscription:
Subscription
Take care of all of themstate
The change of the callback- If the current connection
redux
The component is the first connectionredux
The component, that is, it is connectedredux
The root component of hisstate
Callbacks are registered directly toredux store
; Create a new one at the same timeSubscription
The instancesubscription
throughcontext
Pass to the children.- If the current connection
redux
The component is not a connectionredux
That is, it has a component registered to itredux store
So he can get it throughcontext
hand-me-downsubscription
In the source code, this variable is calledparentSub
The update callback for the current component is registered toparentSub
On. And create a new oneSubscription
Instance, substitutecontext
On thesubscription
“, which means that its child component’s callback is registered with the current onesubscription
On.- when
state
The root component is registered toredux store
The root component needs to manually execute the child component’s callback, which triggers the child component’s update, and then the child component executes itselfsubscription
On the registered callback, the grandson component is triggered to update, and the grandson component is then called to register itselfsubscription
The callback on… This ensures that the child components are updated layer by layer, starting with the root componentFather - > the son
This is the update order.
Subscription
class
So let’s create a new Subscription class:
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.listeners = []; // Source listeners are often linked to lists
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)}// Child component registration callback to Subscription
addNestedSub(listener) {
this.listeners.push(listener)
}
// Perform the child component's callback
notifyNestedSubs() {
const length = this.listeners.length;
for(let i = 0; i < length; i++) {
const callback = this.listeners[i]; callback(); }}// Wrap the callback function
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
// Register the callback function
// If parentSub has a value, the callback is registered with parentSub
// If parentSub has no value, the current component is the root component, and the callback is registered with the Redux store
trySubscribe() {
this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
}
}
Copy the code
Subscription is available here.
transformProvider
In our own react-redux implementation, the root component is always the Provider, so the Provider needs to instantiate a Subscription and place it into the context, and manually invoke the subcomponent callback every time the state is updated.
import React, { useMemo, useEffect } from 'react';
import ReactReduxContext from './Context';
import Subscription from './Subscription';
function Provider(props) {
const {store, children} = props;
// This is the context to pass
// Add store and subscription instances
const contextValue = useMemo(() = > {
const subscription = new Subscription(store)
// Register the callback as the notification child, so that hierarchical notification can begin
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
// Get the previous state value
const previousState = useMemo(() = > store.getState(), [store])
// Every time the contextValue or previousState changes
// notifyNestedSubs is used to notify child components
useEffect(() = > {
const { subscription } = contextValue;
subscription.trySubscribe()
if(previousState ! == store.getState()) { subscription.notifyNestedSubs() } }, [contextValue, previousState])// Returns the ReactReduxContext wrapped component, passing in contextValue
// We will leave children alone
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>)}export default Provider;
Copy the code
transformconnect
With the Subscription class, connect cannot register directly with the Store, but should register with parent Subscription, notifying child components of updates in addition to updating itself. Instead of rendering the wrapped component directly, you should pass in the modified contextValue again using the context.provider wrapper, which should replace the subscription with its own. The code after transformation is as follows:
import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';
function storeStateUpdatesReducer(count) {
return count + 1;
}
function connect(mapStateToProps = () => {}, mapDispatchToProps = () => {}) {
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); / / to the state
// Execute mapStateToProps and mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
const { ...wrapperProps } = props;
const contextValue = useContext(ReactReduxContext);
const { store, subscription: parentSub } = contextValue; // Deconstruct store and parentSub
const actualChildProps = childPropsSelector(store, wrapperProps);
const lastChildProps = useRef();
useLayoutEffect(() = > {
lastChildProps.current = actualChildProps;
}, [actualChildProps]);
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0)
// Create a new subscription instance
const subscription = new Subscription(store, parentSub);
// The state callback is extracted as a method
const checkForUpdates = () = > {
const newChildProps = childPropsSelector(store, wrapperProps);
// If the parameter changes, record the new value to lastChildProps
// And forces the current component to update
if(! shallowEqual(newChildProps, lastChildProps.current)) { lastChildProps.current = newChildProps;// An API is required to force updates to the current component
forceComponentUpdateDispatch();
// Then notify the child of the updatesubscription.notifyNestedSubs(); }};// Register callbacks using subscription
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
// Modify the context passed to the child
// Replace subscription with your own
constoverriddenContextValue = { ... contextValue, subscription }/ / render WrappedComponent
// Use the ReactReduxContext package again, passing in the modified context
return (
<ReactReduxContext.Provider value={overriddenContextValue}>
<WrappedComponent {. actualChildProps} / >
</ReactReduxContext.Provider>)}returnConnectFunction; }}export default connect;
Copy the code
React-redux is now available on GitHub: github.com/dennis-jian…
Let’s summarize the core principles of React-Redux.
conclusion
React-Redux
Is the connectionReact
andRedux
Library, while usingReact
andRedux
The API.React-Redux
Mainly usedReact
thecontext api
To passRedux
thestore
.Provider
The function of is to receiveRedux store
And put it incontext
Pass it on.connect
The role ofRedux store
Select the required properties to pass to the wrapped component.connect
Will make their own judgment whether to update, the basis of judgment is necessarystate
Whether it has changed.connect
Shallow comparison is used when judging whether there is a change, that is, only one layer is compared, so inmapStateToProps
andmapDispatchToProps
Do not revert to multiple nested objects in.- In order to resolve the parent component and child component independent dependency
Redux
, destroyedReact
theParent -> Child
Update process,React-Redux
useSubscription
The class manages its own notification process. - Only connected to
Redux
The top-level components are registered directlyRedux store
All other child components are registered with the nearest parentsubscription
Instance. - When notifying, the root component notifies its children, and when the child component receives the notification, it updates itself and then notifies its children.
The resources
Official documentation: react-redux.js.org/
GitHub source: github.com/reduxjs/rea…
At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.
Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~
“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…
“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…