Redux is a predictable state management tool. The only way to change state is to dispatch an action, which describes how to change state and is sent to reducer to change state.
Create the Redux application
npm i redux
Copy the code
src/index.js:
import {createStore} from 'redux'
const initState = {
list: []}// Reducer, the first parameter of createStore, when the store is initialized, redux will call reducer. State is undefined and action. Type is a random string starting with @@redux/INIT
// The default value of state can be set here to prevent errors when the reducer changes data next time
The reducer should return a state as the new state.
function todo(state = initState, action){
switch(action.type){
case 'todoAdd':
return {
list: state.list.concat(action.text)
}
case 'todoRemove':
return {
list: state.list.filter((v) = >v ! == action.text) }default:
return state
}
}
let store = createStore(todo)
// Subscribe to store updates
store.subscribe(() = > {
console.log(store.getState())
})
// Dispatch an action that is passed to the second parameter on the reducer
store.dispatch({
type: 'todoAdd'.text: 'eat',
})
store.dispatch({
type: 'todoAdd'.text: 'sleep',
})
store.dispatch({
type: 'todoAdd'.text: 'Beat the beans',
})
store.dispatch({
type: 'todoRemove'.text: 'sleep',})Copy the code
The console printed the following result:
Merger of reducer
If there are multiple reducer, one is todo and the other is user data, we can combine reducer with combineReducers provided by Redux, and create a new directory under SRC as store, SRC /store/index.js as the created store. SRC /store/todo.js and SRC /store/user.js are reducer of Todo and reducer of user, respectively. SRC /store/index.js:
import {createStore, combineReducers} from 'redux'
import todo from './todo'
import user from './user'
const reducer = combineReducers({
todo,
user,
})
const store = createStore(reducer)
export default store
Copy the code
SRC/store/user. For the js code
const initState = {
name: 'xiaobai'.age: 18,}function user(state = initState, action){
switch(action.type){
case 'userAgeAdd':
return {
...state,
age: state.age + 1,}case 'userNameChange':
return {
...state,
name: action.name,
}
default:
return state
}
}
export default user
Copy the code
Now add a bit of code to SRC /index.js
store.dispatch({
type: 'userNameChange'.name: 'xiaohei',})Copy the code
Print the result
Create the Action generator
Dispatch needs to write an action each time, but it would be more convenient if we package it into a function to return an action. We start with the reducer as user to package the function that generates the action. SRC /store/user.js adds two functions
export function userNameChange(name){
return {
type: 'userNameChange',
name,
}
}
export function userAgeAdd(){
return {
type: 'userAgeAdd',}}Copy the code
Change SRC /index.js to:
import store from './store'
import {userNameChange} from './store/user'
// Subscribe to store updates
store.subscribe(() = > {
console.log(store.getState())
})
store.dispatch(userNameChange('xiaohei'))
Copy the code
The user. Name has been changed to ‘xiaohei’.
Create an asynchronous action generator
Reducer is a pure function and should not modify incoming parameters, no API requests and route jumps with side effects should be executed, and no impure functions should be called. As long as the parameters passed in are the same, the next state returned from the calculation must be the same. To do this, you need a plug-in called Redux-thunk. Using applyMiddleware provided by Redux, action creation functions can return functions in addition to action objects, and when returned, the function will be executed, taking a parameter of Dispatch. This function doesn’t have to be pure.
npm i redux-thunk
Copy the code
Continue to modify the userNameChange exported from SRC /store/user.js
export function userNameChange(name){
return (dispatch) = > { // The returned function is executed and passed to Dispatch
setTimeout(() = > { // Simulate an API request
console.log('Dispatch an action in one second')
dispatch({
type: 'userNameChange',
name,
})
},2000)}}Copy the code
/src/store/index.js
import {createStore, combineReducers, applyMiddleware} from 'redux'
import reduxThunk from 'redux-thunk'
import todo from './todo'
import user from './user'
const reducer = combineReducers({
todo,
user,
})
const store = createStore(reducer, applyMiddleware(
reduxThunk
))
export default store
Copy the code
Now open the console and refresh the page. After a second, the result will print normally, indicating that we have completed the asynchronous action. Redux-thunk isn’t the only way for Redux to handle asynchronous operations; you can also write a custom Middleware when you finish reading the next section
Middleware analysis
Middleware is code that can be embedded in the framework to receive requests and generate responses. For example, Express or Koa’s Middleware can add CORS Headers, log, compress content, and more. One of the best properties of Middleware is that it can be chained together. You can use multiple independent third party Middleware in a single project. Redux Middleware provides extension points after an action has been initiated and before it arrives at the Reducer. You can use Redux Middleware for logging, creating crash reports, calling asynchronous interfaces, routing, and more.
Manually Recording Logs
If we don’t have applyMiddleware from Redux, we might need to do this manually if we want to log redux. SRC /store/user.js changed to userNameChange and SRC /store/index.js removed middleware. src/index.js
import store from './store'
import {userNameChange} from './store/user'
let action = userNameChange('xiaohei')
console.log('dispatch', action.type)
store.dispatch(action)
console.log('newState', store.getState())
Copy the code
Rewrite the dispatch
Although the above method can realize the function of logging, it needs to record every dispatch. Since it is inevitable to use Dispatch to change data, we can try to rewrite Dispatch and add some operations that we need to do while retaining the complete functions of the original dispatch. src/index.js
import store from './store'
import {userNameChange} from './store/user'
let next = store.dispatch // Save the full functionality of the original dispatch
store.dispatch = (action) = > { // Override dispatch to receive an action
console.log('dispatch', action.type)
let result = next(action) // Perform the original dispatch function
console.log('newState', store.getState())
return result
}
let action = userNameChange('xiaohei')
store.dispatch(action)
Copy the code
Now open the console, no matter where dispatch is, you can log normally.
New middleware
Catching exceptions was also important in real development, and now if new functionality was needed, writing new functionality on top of the original rewritten Dispatch would make the code look messy, so we could have written two separate functionality. src/index.js
import store from './store'
import {userNameChange} from './store/user'
const logMiddleware = (store) = > {
let next = store.dispatch
store.dispatch = (action) = > {
console.log('dispatch', action.type)
let result = next(action)
console.log('newState', store.getState())
return result
}
}
const errMiddlware = (store) = > {
let next = store.dispatch
store.dispatch = (action) = > {
try {
return next(action)
}catch(err){
console.log('redux throws an exception ')
throw err
}
}
}
logMiddleware(store)
errMiddlware(store)
let action = userNameChange('xiaohei')
store.dispatch(action)
// To test exception catching, dispatch does not pass parameters.
store.dispatch()
Copy the code
Now open the console and print the result asAs expected, both middleware functions are implemented, and the code runs as follows: logMiddleware executes, passing in the store generated by createStore, and logMiddleware overwrites the Store dispatch method. ErrMiddlware is passed in a store whose dispatch method has been overridden by the logMiddleware method, and errMiddlware continues to add functionality to the original dispatch method.
ApplyMiddleware source
Study the source code for applyMiddleware provided by Redux
function applyMiddleware(. middlewares) {
//createStore determines that if the applyMiddleware return function is executed, the store creation is left to the code below
// Return a store that processed dispatch, now... Args is the reducer we passed in.
Reducer createStore (reducer)
return (createStore) = > (. args) = > {
conststore = createStore(... args)let dispatch = () = > {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.')}const middlewareAPI = { // Middleware accessible parameters
getState: store.getState,
dispatch: (. args) = >dispatch(... args) }const chain = middlewares.map(middleware= > middleware(middlewareAPI)), // Receives next, returns an array of functions to dispatch.dispatch = compose(... chain)(store.dispatch)// The function generated by the original dispatch incoming compose is chain-handled.
return {
...store,
dispatch // Processed dispatches}}}Copy the code
To compose below:
export default function compose(. funcs) {
if (funcs.length === 0) {
return arg= > arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = > {
return (. args) = >a(b(... args)) }) }//compose is composed for an arbitrary number of functions, such as funcA, funcB, funcC,
// generate a new function (... args) => funcA(funcB(funcC(... args)))
// It means that each function passes in the return value of one function as an argument, and takes its calculated return value as the argument of the next function.
Copy the code
Middleware is a function that receives the middlewareAPI, returns a function that takes next, and a function that returns next as the next middleware.
Using applyMiddleware
Start by overwriting logMiddleware and errMiddlware SRC /store/index.js
import {createStore, combineReducers, applyMiddleware} from 'redux'
import reduxThunk from 'redux-thunk'
import todo from './todo'
import user from './user'
const reducer = combineReducers({
todo,
user,
})
const logMiddleware = (store) = > { // Store is the middlewareAPI from applyMiddleware
return (next) = > { The callback function receives next and returns dispatch as the next middleware argument
return (action) = > { //dispatch
console.log('dispatch', action.type)
let result = next(action)
console.log('newState', store.getState())
return result
}
}
}
const errMiddlware = (store) = > { //middlewareAPI
return (next) = > { //logMiddleware returns a dispatch
return (action) = > { // Return to dispatch as the next middleware
try {
return next(action)
}catch(err){
console.log('redux throws an exception ')
throw err
}
}
}
}
const store = createStore(reducer, applyMiddleware(
//middlewares
logMiddleware,
errMiddlware,
))
export default store
Copy the code
SRC /index.js deletes the dispatch processing.
import store from './store'
import {userNameChange} from './store/user'
store.dispatch(userNameChange('xiaohei'))
// To test exception catching, dispatch does not pass parameters.
store.dispatch()
Copy the code
Now open the console and we have both redux logs and exception catches, so we’re writing middleware correctly and we’re going to currize them.
const logMiddleware = (store) = > (next) = > (action) = > {
console.log('dispatch', action.type)
let result = next(action)
console.log('newState', store.getState())
return result
}
const errMiddlware = (store) = > (next) = > (action) = > {
try {
return next(action)
}catch(err){
console.log('redux throws an exception ')
throw err
}
}
Copy the code
Asynchronous action
The elegant addition of logMiddleware and errMiddleware does not yet support asynchronous actions, but adds another middleware that allows actions to return a function. The function handles asynchronous operations and eventually dispatches an action to change the data. SRC /store/user.js add an asynchronous action
export function userAgeAddSync(){
return (dispatch) = > { // Receive dispatch is used to dispatch an asynchronous operation after completion
setTimeout(() = > {
dispatch(userAgeAdd())
}, 1000)}}Copy the code
Refresh the page and redux throws an exceptionBecause userAgeAddSync results in a function that receives dispatch, and this is why action.type is undefined, you need to add middleware to handle this asynchronous action. SRC /store/index.js adds another middleware and applies it to applyMiddleware
const syncMiddlware = (store) = > (next) = > (action) = > {
if(typeof action === 'function') {// If action is a function, execute the function directly, passing in dispatch
action(store.dispatch)
}else{
return next(action)
}
}
Copy the code
Now look at the printNow we’ve done that with userAgeAdd after a second, but there’s a step that prints action.type as undefined, and if you’re familiar with the Middleware chain calls, you already know why. Because the function returned by userAgeAddSync() is processed by logMiddleware when we execute store.dispatch(userAgeAddSync()), the action. Type must be undefined, Now it’s just a matter of checking in logMiddleware and executing action if it’s a function, passing in dispatch, and logging logic if it’s not. Change the logMiddleware code to
const logMiddleware = (store) = > (next) = > (action) = > {
let result
if(typeof action === 'function'){
action(store.dispatch)
}else{
console.log('dispatch', action.type)
result = next(action)
console.log('newState', store.getState())
return result
}
}
Copy the code
Now open the console, the page is loaded and only the log of userNameChange is displayed. After a second, the log of userAgeAdd is printed, which is exactly what we want. If I have time later, I will encapsulate this code and apply it to React. I have posted the code for this blog and all chapter submissions on Gitee, where interested students can download the study.