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.
mapStateToProps
The function gives you controlstore
Which states are mapped to the props of the component.mapDispatchToProps
So you can encapsulate it yourselfdispatch
Delta function and map to deltaprops
In 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
useStore
Get the store objectuseSelector
Use the selector function fromstate
Extract data fromuseDipatch
Get 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!