Article series
Hand-written stupid React family bucket Redux
The React-Redux of the React family bucket
In this paper, the code
What is a Redux
Predictable State Container for JS Apps
Predictable: actually refers to pure functions (each input has a fixed output with no side effects). This is guaranteed by the Reducer and also facilitates testing
State container: In a Web page, each DOM element has its own state. For example, the pop-up box has two states: show and hide. The current page of the list and how many items are displayed on each page are the state of the list.
Although Redux is part of the React family, Redux is not necessarily related to React. It can be used on any other library, but it is popular with React.
The core concept of Redux
- Store: A container that stores state, a JS object
- Actions: Object that describes what Actions to take on the state
- Reducers: Function that operates on a state and returns a new state
- React Component: A Component, or view
Redux workflow
- The component fires the Action through the Dispatch method
- Store Receives the Action and distributes the Action to the Reducer
- The Reducer changes the state based on the Action type and returns the changed state to the Store
- The component subscribes to the state in the Store, and status updates from the Store are synchronized to the component
Here are the following:
React Components
It’s the React component, which is the UI layer
Store
Manage the warehouse of data and expose some apis to the outside world
let store = createStore(reducers);
Copy the code
Has the following responsibilities:
- Maintain the state of the application;
- provide
getState()
Method to obtain the state; - provide
dispatch(action)
Method update state; - through
subscribe(listener)
Register listeners; - through
subscribe(listener)
The returned function unlogs the listener.
Action
An action is an action in a component that dispatches (action) to trigger changes in the store
Reducer
Action is just a command that tells the Store what changes to make. The reducer really changes the data.
Why use Redux
By default, React can only pass data from top to bottom, but when the lower component wants to pass data to the upper component, the upper component needs to pass the method of modifying the data to the lower component. When the project gets bigger and bigger, this way of passing will be very messy.
The reference to Redux, because Store is independent of components, makes the data management independent of components, to solve the problem of difficult data transfer between components
counter
Define store container files and generate stores according to the Reducer
import { createStore } from "redux";
const counterReducer = (state = 0, { type, payload = 1 }) = > {
switch (type) {
case "ADD":
return state + payload;
case "MINUS":
return state - payload;
default:
returnstate; }};export default createStore(counterReducer);
Copy the code
In the component
import React, { Component } from "react";
import store from ".. /store";
export default class Redux extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() = > {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
add = () = > {
store.dispatch({ type: "ADD".payload: 1 });
};
minus = () = > {
store.dispatch({ type: "MINUS".payload: 1 });
};
render() {
return (
<div className="border">
<h3>Adder subtracter</h3>
<button onClick={this.add}>add</button>
<span style={{ marginLeft: "10px", marginRight: "10px}} ">
{store.getState()}
</span>
<button onClick={this.minus}>minus</button>
</div>); }}Copy the code
- The getState command displays state
- When add or Minus is clicked, dispatch is triggered and action is passed
- And listens for state changes on componentDidMount and forces rendering on forceUpdate
- ComponentWillUnmount Clears listeners
3. Start writing
As you can see from the above, the main function is the createStore function, which exposes the getState, Dispatch and subScribe functions, so let’s go down and create the createstore.js file
export default function createStore(reducer) {
let currentState;
// Obtain the state of the store
function getState() {}
/ / change the store
function dispatch() {}
// Subscribe to store changes
function subscribe() {}
return {
getState,
dispatch,
subscribe,
};
}
Copy the code
Then perfect the next method
getState
Returns the current state
function getState() {
return currentState
}
Copy the code
dispatch
Receive the action and update the store, by whom: Reducer
/ / change the store
function dispatch(action) {
// Pass the current state and action to the Reducer function
// Return the new state stored in currentState
currentState = reducer(currentState, action);
}
Copy the code
subscribe
Function: Subscribe to changes in state
How to do it: In observer mode, the component listens to the SUBSCRIBE, passes in a callback function, registers the callback in the subscribe, and fires the callback in the Dispatch method
let curerntListeners = [];
// Subscribe to the state change
function subscribe(listener) {
curerntListeners.push(listener);
return () = > {
const index = curerntListeners.indexOf(listener);
curerntListeners.splice(index, 1);
};
}
Copy the code
The dispatch method executes a subscription event after updating the data.
/ / change the store
function dispatch(action) {
// The data in the store is updated
currentState = reducer(currentState, action);
// Execute the subscription event
curerntListeners.forEach(listener= > listener());
}
Copy the code
The complete code
Change the redux in the counter above to refer to the handwritten redux, and you’ll see that the page has no initial value
So add dispatch({type: “KKK”}) to the createStore; If state is assigned an initial value, make sure that the type is entered into the reducer’s default condition
The complete code is as follows:
export default function createStore(reducer) {
let currentState;
let curerntListeners = [];
// Obtain the state of the store
function getState() {
return currentState;
}
/ / change the store
function dispatch(action) {
// Pass the current state and action to the Reducer function
// Return the new state stored in currentState
currentState = reducer(currentState, action);
// Execute the subscription event
curerntListeners.forEach((listener) = > listener());
}
// Subscribe to the state change
function subscribe(listener) {
curerntListeners.push(listener);
return () = > {
const index = curerntListeners.indexOf(listener);
curerntListeners.splice(index, 1);
};
}
dispatch({ type: "kkk" });
return {
getState,
dispatch,
subscribe,
};
}
Copy the code
You can also check the Redux createStore source code
Redux middleware
When it comes to Redux, middleware is the only thing Redux can do, such as redux-Thunk for asynchronous calls, redux-Logger for logging, etc. The middleware is a function. Adding other functions between the Action and Reducer steps is equivalent to enhancing dispatch.
Develop Redux middleware
There is template code for developing middleware
export default store => next= > action= > {}
Copy the code
- A middleware receives the store as an argument and returns a function
- The returned function takes next (the old Dispatch function) as an argument and returns a new function
- The new function returned is the enhanced Dispatch function, which takes the store and next passed in from the top two levels
For example, simulate writing a Logger middleware
function logger(store) {
return (next) = > {
return (action) = > {
console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
console.log(action.type + "Done!);
console.log("prev state", store.getState());
next(action);
console.log("next state", store.getState());
console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
};
};
}
export default logger;
Copy the code
Registration middleware
// Pass applyMiddleware as the second parameter on the createStore
const store = createStore(
reducer,
applyMiddleware(logger)
)
Copy the code
As you can see, this is done through the createStore’s second parameter, officially called enhancer. Enhancer is a function that takes the createStore parameter and returns a new createStore function
function enhancer (createStore) {
return function (reducer) {
var store = createStore(reducer);
var dispatch = store.dispatch;
function _dispatch (action) {
if (typeof action === 'function') {
return action(dispatch)
}
dispatch(action);
}
return {
...store,
dispatch: _dispatch
}
}
}
Copy the code
The createStore implementation has three parameters, the first reducer parameter is mandatory, the second state initial value, and the third enhancer is optional
Let’s add the enhancer parameter to the handwriting createStore and the handwriting applyMiddleware function
CreateStore plus enhancer
function createStore(reducer,enhancer) {
// Determine whether enhancer exists
// If it exists and is a function, pass createStore to it; if not, throw an error
// It returns a new createStore
// Enter the Reducer, execute the new createStore, and return the Store
// Return the store
if (typeofenhancer ! = ='undefined') {
if (typeofenhancer ! = ='function') {
throw new Error('Enhancer must be a function.')}return enhancer(createStore)(reducer)
}
// No enhancer goes through the original logic
/ / to omit
}
Copy the code
Handwritten applyMiddleware
The applyMiddleware function receives the middleware function and returns an enhancer, so the basic structure is
export default function applyMiddleware(. middlewares) {
// applyMiddleware should return an enhancer
// Enhancer is a function that takes createStore as an argument
return function (createStore) {
// Enhancer to return a new createStore
return function newCreateStore(reducer) {};
};
}
Copy the code
Looking at the structure of the Logger middleware,
function logger(store) {
return (next) = > {
return (action) = > {
console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
console.log(action.type + "Done!);
console.log("prev state", store.getState());
next(action);
console.log("next state", store.getState());
console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
};
};
}
export default logger;
Copy the code
Under the perfect applyMiddleware
export default function applyMiddleware(middleware) {
// applyMiddleware should return an enhancer
// Enhancer is a function that takes createStore as an argument
return function (createStore) {
// Enhancer to return a new createStore
return function newCreateStore(reducer) {
/ / create a store
let store = createStore(reducer);
let dispatch = store.dispatch;
// The dispatch property must be written in this form. You cannot pass in store.dispatch directly
// When there are multiple middleware, the value of dispatch is to get the enhanced dispatch of the previous middleware
// This is valid because Dispatch is a reference type
const midApi = {
getState: store.getState,
dispatch: (action, ... args) = >dispatch(action, ... args), };// Pass store to execute layer 1 functions of the middleware
const chain = middleware(midApi);
// Pass the original dispatch function to the chain as the next argument, calling the middleware layer 2 function
// Return the enhanced dispatch to override the original dispatch
dispatch = chain(dispatch);
return {
...store,
dispatch,
};
};
};
}
Copy the code
Testing:Logs can be printed normally
Support for multiple middleware
The applyMiddleware function above handles only one middleware. What about multiple middleware scenarios?
First, let’s simulate writing a redux-Thunk middleware
By default, Redux only supports synchronization, and the arguments can only be objects. What Redux-Thunk implements is that when you pass a function, I execute that function directly, and the asynchronous operation code is written in the passed function, and if an object is passed, the next middleware is called
function thunk({ getState, dispatch }) {
return next= > {
return action= > {
// If it is a function, execute it directly, passing in Dispatch and getState
if (typeof action == 'function') {
return action(dispatch, getState)
}
next(action)
}
}
}
Copy the code
Now it’s time to execute each piece of middleware in sequence. How? I’m going to curryize it, and I’m going to write the compose function
function compose(. funs) {
// If there is no pass-through function, return the pass-through function
if (funs.length === 0) {
return (arg) = > arg
}
// When a function is passed, the function is returned, eliminating the need for traversal
if (funs.length === 1) {
return funs[0]}// When multiple items are passed, reduce is used to merge them
For example, compose(f1,f2,f3) will return (... args) => f1(f2(f3(... args)))
return funs.reduce((a, b) = > {
return (. args) = > {
returna(b(... args)) } }) }Copy the code
The applyMiddleware function supports multiple middleware:
export default function applyMiddleware(. middlewares) {
// applyMiddleware should return an enhancer
// Enhancer is a function that takes createStore as an argument
return function (createStore) {
// Enhancer to return a new createStore
return function newCreateStore(reducer) {
/ / create a store
let store = createStore(reducer);
let dispatch = store.dispatch;
// The dispatch property must be written in this form. You cannot pass in store.dispatch directly
// When there are multiple middleware, the value of dispatch is to get the enhanced dispatch of the previous middleware
// This is valid because Dispatch is a reference type
const midApi = {
getState: store.getState,
dispatch: (action, ... args) = >dispatch(action, ... args), };// Pass a neutered version of the Store object by calling the middleware layer 1 function
const middlewareChain = middlewares.map((middle) = > middle(midApi));
// Use compose to get a function that combines all the middleware components
constmiddleCompose = compose(... middlewareChain);// Call the middleware layer 2 functions one by one with the original dispatch function as an argument
// Return the enhanced dispatch to override the original dispatch
dispatch = middleCompose(dispatch);
return {
...store,
dispatch,
};
};
};
}
Copy the code
Validation:
import { createStore } from ".. /kredux";
import logger from ".. /kredux/middlewares/logger";
import thunk from ".. /kredux/middlewares/thunk";
import applyMiddleware from ".. /kredux/applyMiddleware";
const counterReducer = (state = 0, { type, payload = 1 }) = > {
switch (type) {
case "ADD":
return state + payload;
case "MINUS":
return state - payload;
default:
returnstate; }};export default createStore(counterReducer, applyMiddleware(thunk, logger));
Copy the code
Change the add function to asynchronously trigger Dispatch
add = () = > {
// store.dispatch({ type: "ADD", payload: 1 });
store.dispatch(function (dispatch) {
setTimeout(() = > {
dispatch({ type: "ADD".payload: 2 });
}, 1000);
});
};
Copy the code
Five, handwritten combineReducers
When the business logic is complex and it is not possible to write all the reducers in one reducer, use combineReducers to combine several reducers.
Add another userReducer:
const userReducer = (state = { ... initialUser }, { type, payload }) = > {
switch (type) {
case "SET":
return{... state, ... payload };default:
returnstate; }};Copy the code
Introduce the combineReducers function. This function accepts several objects. The key is the identifier and the value is each Reducer
export default createStore(
combineReducers({ count: counterReducer, user: userReducer }),
applyMiddleware(thunk, logger)
);
Copy the code
Write combineReducers, which return reducer functions. Naturally, the reducer function should receive state and action and return the new state
export default function combineReducers(reducers) {
return function reducer(state = {}, action) {
let nextState = {};
// Iterate over all reducers, triggering the return of new states in turn
for (let key in reducers) {
nextState[key] = reducers[key](state[key], action);
}
return nextState;
};
}
Copy the code
Six, summarized
- The Redux itself is just a predictable state container. All state changes are made by sending the Action command to the Reducer. The Action command is then distributed to the Reducer, and the Reducer returns the new state value.
- Redux is a typical observer pattern, registering a callback event when subscribe and executing a callback when dispatch action
- Redux focuses on the createStore function, which returns three methods, getState to return the current state, subscribe to subscribe to changes in state, Dispatch to update the store, and execute a callback event
- The default Redux supports only incoming objects and can only perform synchronization, so middleware is needed for more scenarios
- Middleware is a template for
export default store => next => action => {}
The function of - To register the middleware, use the createStore enhancer
- Redux middleware is a enhancement of Dispatch, which is a typical decorator pattern, and you can see that Redux is full of design patterns