My redux puzzle journey

My redux puzzle journey

As a member of the React family bucket, redux is often used in projects. When I first came into contact with redux, I was always a little confused :

  1. What is a reducer?
  2. Why do I need combineReducers?
  3. What is the mechanism of middlewares?

Through reading the official website of redux , these doubts are slowly solved.
1. let me take a picture of my understanding of the overall process of redux:

The reducer and optional preloadState and applyMiddleware are passed into createStore to create a store object with dispatch, getState, and subscribe methods

Let s start with the reducer,

The reducer is actually the specific logic for updating the state
:

function reducer ( prevState, action ) { const {type, payload} = action switch (type) { //... default : return prevState } } Copy code

And dispatch updates the state by calling the reducer:

store.dispatch = ( action ) => { store.state = reducer(store.state, action) } Copy code

So the process after triggering dispatch is

dispatch(action) => reducer(prevState, action) => newState
:

When the state data structure becomes more and more complex, the reducer will contain the update logic of different dimensional data, such as:

let state = { user : { id : 1 , name : 'Daniel Yang' , age : 26 , avatar : '' }, role : { employer : true , developer : true , manager : false }, list : [] } function the reducer ( PrevState = State, Action ) { const {type, payload} = Action Switch (type) { Case 'UPDATE_USER' : return {... PrevState, User : payload} Case 'UPDATE_ROLE' : return {... PrevState , Role : payload} Case 'REMOVE_ROLE' : return {... PrevState, Role : prevState.role.filter ( R & lt => ! R & lt == payload.id)} Case 'add_list' : return {... PrevState, List : prevState.list.concat (payload)} Case 'REMOVE_LIST' : return {... PrevState, List : list.filter ( L => L == payload!. id)} default : return prevState } } Copy code

If we can split the data into separate different dimensions of reducer, and then merged together by a certain method to create createStore pass into the store, the whole logic will clear a lot
so

combineReducers
Come into being :

function combineReducers ( reducerObjs ) { return ( prevState, action ) => { const combineState = {} for ( const key of Object .keys(reducerObjs)) { //Get sub reducer methods and corresponding data const reducer = reducerObjs[key] const state = prevState[key] combineState[key] = reducer(state, action) } return { ...prevState, ...combineState } } } Copy code

Now we can split our reducer like this:

function userReducer ( PrevState, Action ) { const {type, payload} = Action Switch (type) { Case 'UPDATE_USER' : return payload default : return PrevState } } function roleReducer ( PrevState, Action ) { const {type, payload} = Action Switch (type) { Case 'UPDATE_ROLE' : return payload Case 'REMOVE_ROLE' : return {... PrevState, Role : prevState.filter ( R & lt => R & lt !== payload.id)} default : return prevState } } function listReducer ( prevState, action ) { const {type, payload} = action Switch (type) { Case 'add_list' : return {... PrevState, List : prevState.concat (payload)} Case 'REMOVE_LIST' : return {. ..prevState, list : prevState.filter( l => l !== payload.id)} default : return prevState } } const reducer = combineReducers({ user : userReducer, role : roleReducer, list : listReducer }) Copy code

With the deepening of use, logging, error capture, asynchronous data flow, etc. are common requirements.
Taking logging as an example, if we want to record every action issued and updated state,

How can it be realized on the existing basis?

Try one: We can add print before and after each call to dispatch:

console .info( 'action: ' , action) store.dispatch(action) Console .info ( 'State:' , store.getState ()) copying the code

But it is obviously not suitable. Every time we initiate a dispatch, we have to manually write the printing method before and after, the method is too primitive

Attempt 2: How about we rewrite the store.dispatch method :

const next = store.dispatch store.dispatch = action => { console .info( 'action: ' , action) next(action) console .info( 'state: ' , store.getState()) } Copy code

Now when we call the dispatch method, the action and state will be printed before and after, it looks perfect :)
But what if we have multiple extensions that we want to apply during dispatch , such as adding error capture:

const next = store.dispatch store.dispatch = action => { try { next(action) } catch (err) { console .error(err) } } Copy code

Attempt 3: So it is natural to think of encapsulating store.dispatch method :

function addLog ( store ) { let next = store.dispatch store.dispatch = action => { console .info( 'action: ' , action) next(action) console .info( 'state: ' , store.getState()) } } function handleError ( store ) { let next = store.dispatch store.dispatch = action => { try { next(action) } catch (err) { console .error(err) } } } addLog(store) handleError(store) Copy code

It can be used now, but then think about whether the middleware can be optimized into a chain call instead of manually calling the encapsulated method every time it is used?

Attempt 4: You can return to the extended dispatch , so that multiple middlewares can be connected in series to get the final dispatch:

//Middleware example function middleware ( store ) { let next = store.dispatch return action => { //... do something return next(action) } } function applyMiddleware ( store, middlewares ) { middlewares.forEach( middleware => { store.dispatch = middleware(store) }) } Copy code

Attempt 5: Finally, if we pass dispatch as a parameter instead of getting it through store.dispatch inside the function every time, wouldn t it be better?:

//Middleware example function middleware ( store ) { return next => { return action => { //do something return next(action) } } } //Correspondingly, applyMiddleware is adjusted function applyMiddleware ( store, middlewares ) { //Adjust the order of middleware, it is the order in which the middleware is passed in when the package is executed middlewares = middlewares.reverse() let dispatch = store.dispatch //The dispatch returned by the previous middleware will be used as the next middlewares of the next middleware.forEach( middleware => { dispatch = middleware(store)(dispatch) }) return {...store, dispatch} } Copy code

This is very close to the implementation of applyMiddleware in the redux source code. In contrast, the source code implementation has several more perfect points:

  1. The top-level store parameter only exposes the necessary methods, not the entire store object;
  2. If you call store.dispatch directly in middleware instead of next(action), then the entire middleware chain will be traversed again, which is very useful for asynchronous middleware;
  3. In order to ensure that the middleware is only applied once, it will act on createStore() instead of the store itself.

The approximate implementation of the source code:

Focus on the applyMiddleware part

function createStore ( reducer, preloadState, applyMiddleware ) { return applyMiddleware(createStore)(reducer, preloadState) } function applyMiddleware ( ...middlewares ) { return ( createStore ) => ( ...args ) => { //Create store const store = createStore(...args) //Only pass necessary methods to middleware const middlewareAPI = { getState : store.getState, dispatch : ( ...args ) => dispatch(...args) } const chain = middlewares.map( middleware => middleware(middlewareAPI)) //Pass store.dispatch to the combined middleware chain and reassign dispatch //If store.dispatch is called directly in the middleware, it will go again compose(...chain)(store.dispatch) method dispatch = compose(...chain)(store.dispatch) //Return the new store object return { ...store, dispatch } } } //Combined middleware chain: (e, f, g) => (...args) => e(f(g(...args))) function compose ( ...funcs ) { return funcs.reduce ( ( a, b ) => ( ...args ) => a(b(...args))) } Copy code

Finally, let's take a look at how to implement asynchronous data flow through middleware, taking redux-thunk as an example:

function reduxThunkMiddleware ( store ) { return function ( dispatch ) { return function ( action ) { //If the sent action is a method, pass the agreed parameters to the action execution //You can decide when to send dispatch inside the action method if ( typeof action === 'function' ) { return action(dispatch, store.getState) } return dispatch(action) } } } Copy code

Now for the question mentioned at the beginning, do you have your own answer :

  1. What is a reducer?
  2. Why do I need combineReducers?
  3. What is the mechanism of middlewares?