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:
- View: The View layer, which can be seen as a component in React
- Action: A message sent when an Action is performed in the View
- Dispatch: sender that receives an Action and informs the Store to change data based on the Action
- 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:
- The user accesses a View, which takes the state from the Store to render the page
- When the user performs an Action on the View, such as clicking, an Action is generated for the Action
- After the Action is generated, it is sent to the Store via Dispatch for status updates
- 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:
- Store: Saves the status of an application
- State: indicates the status of the application
- Action: Have one
type
Property, representing changesstate
The intent of the - Dispatch: Receives one
action
Sent to thestore
- Reducer: Reducer is a pure function that receives the last one
state
andaction
, through the last onestate
andaction
The data is recalculated to newstate
Make a return update. - Middleware: Middleware that uses high-order function pairs
dispatch
Returns an enhanceddispatch
function
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:
- Storage condition
- Changes in state that need to be listened on, and UI components need to be notified of state updates (subscriber mode)
- 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:
- Need to pass to middleware
store
The API:Dispatch, getState
- Multiple middleware enhancements need to be combined
dispatch
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:
- Provider: the Provider of a store that passes the STORE API across hierarchies
- Connect: Connects the React component to the Redux store
- UseSelector: The hook function that gets the specified state
- 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:
- Connect the React component to Redux’s Store to pass properties like state and Dispatch to the React component
- 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:
- 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.
- 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:
- You need to pass in a callback function that takes state and returns the specified state.
- 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