This article will write Redux from scratch. If you are already familiar with the use and source code of Redux, you will only need to skim it. If you are not, follow this article to implement your Redux. Know what it is and why.

The code for this article is at: github.com/YvetteLau/B… , it is recommended to clone the code and read this article against the code.

1. ReduxWhat is?

Redux is a JavaScript state container that provides predictable state management. Redux also supports other interface libraries in addition to React. Redux is small and lean, just 2KB. There is no strong binding between Redux and React. This article aims to understand and implement a Redux, but not react-Redux. (You can dive into react-Redux one at a time, and react-Redux will appear in the next article.)

2. Build one from scratchRedux

Let’s forget about Redux and start with an example of creating a project using create-React-app: to-redux.

Code address: myredux/to-redux

Change the body in public/index.html to the following:

<div id="app">
    <div id="header">Front end of the universe</div>
    <div id="main">
        <div id="content">Hello, I'm Xiaoxi Liu, the author of front-end Universe</div>
        <button class="change-theme" id="to-blue">Blue</button>
        <button class="change-theme" id="to-pink">Pink</button>
    </div>
</div>
Copy the code

What we want to do is to change the color of the font across the entire app when we click the button.

Modify SRC /index.js as follows (to-redux/ SRC /index1.js):

let state = {
    color: 'blue'
}
// Render the application
function renderApp() {
    renderHeader();
    renderContent();
}
// Render the title section
function renderHeader() {
    const header = document.getElementById('header');
    header.style.color = state.color;
}
// Render the content section
function renderContent() {
    const content = document.getElementById('content');
    content.style.color = state.color;
}

renderApp();

// Click the button to change the font color
document.getElementById('to-blue').onclick = function () {
    state.color = 'rgb(0, 51, 254)';
    renderApp();
}
document.getElementById('to-pink').onclick = function () {
    state.color = 'rgb(247, 109, 132)'; 
    renderApp();
}
Copy the code

This application is very simple, but there is one problem: State is a shared state, but anyone can change it, and if we arbitrarily change the state, it can lead to errors, such as setting state = {} in the renderHeader, which can cause unexpected errors.

Most of the time, however, we do need to share state, so we can consider setting some threshold. For example, we can agree that we can’t change the global state directly, but must change it through some path. To do this, we define a changeState function that is responsible for changing the global state.

// Continue appending code in index.js
function changeState(action) {
    switch(action.type) {
        case 'CHANGE_COLOR':
            return {
                ...state,
                color: action.color
            }
        default:
            returnstate; }}Copy the code

The convention is that you can only change the state with changeState, which takes an action, a plain object with a Type field that identifies the type of action you are doing (how to change the state).

We want the button to change the font color for the entire app.

// Continue appending code in index.js
document.getElementById('to-blue').onclick = function() {
    let state = changeState({
        type: 'CHANGE_COLOR'.color: 'rgb(0, 51, 254)'
    });
    // After the state is changed, the page needs to be re-rendered
    renderApp(state);
}

document.getElementById('to-pink').onclick = function() {
    let state = changeState({
        type: 'CHANGE_COLOR'.color: 'rgb(247, 109, 132)'
    });
    renderApp(state);
}
Copy the code

Out of the store

Although we now have a convention on how to change the state, state is a global variable that we can easily change, so we can consider making it a local variable, defining it inside a function (createStore), but also using state outside, So we need to provide a method getState() so that we can getState outside of createStore.

function createStore (state) {
    const getState = (a)= > state;
    return {
        getState
    }
}
Copy the code

Now, we can retrieve the state using the store.getState() method. (State is usually an object, so it can be directly modified externally, but if a deep copy of state returns, it cannot be modified externally.) Since state is returned directly in the Redux source code, we don’t want to make a deep copy here (performance cost, after all).

It’s not enough just to get the state, we also need to have methods to modify the state, now that the state is a private variable, we must also put the methods to modify the state in createStore and expose them to external use.

function createStore (state) {
    const getState = (a)= > state;
    const changeState = (a)= > {
        / /... The code in changeState
    }
    return {
        getState,
        changeState
    }
}
Copy the code

Now the code in index.js looks like this (to-redux/ SRC /index2.js):

function createStore() {
    let state = {
        color: 'blue'
    }
    const getState = (a)= > state;
    function changeState(action) {
        switch (action.type) {
            case 'CHANGE_COLOR': state = { ... state,color: action.color
                }
                return state;
            default:
                returnstate; }}return {
        getState,
        changeState
    }
}

function renderApp(state) {
    renderHeader(state);
    renderContent(state);
}
function renderHeader(state) {
    const header = document.getElementById('header');
    header.style.color = state.color;
}
function renderContent(state) {
    const content = document.getElementById('content');
    content.style.color = state.color;
}

document.getElementById('to-blue').onclick = function () {
    store.changeState({
        type: 'CHANGE_COLOR'.color: 'rgb(0, 51, 254)'
    });
    renderApp(store.getState());
}
document.getElementById('to-pink').onclick = function () {
    store.changeState({
        type: 'CHANGE_COLOR'.color: 'rgb(247, 109, 132)'
    });
    renderApp(store.getState());
}
const store = createStore();
renderApp(store.getState());
Copy the code

Although we have now removed the createStore method, it is clear that this method is not at all generic, as both the state and changeState methods are defined in the createStore. In this case, other applications cannot reuse this pattern.

The logic of changeState should be defined externally, because the logic of changing state is necessarily different for each application. We stripped this logic externally and renamed it reducer (to be consistent with Redux). Reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer reducer Because it is not defined within createStore and does not have direct access to state, we need to pass it the current state as a parameter. As follows:

function reducer(state, action) {
    switch(action.type) {
        case 'CHANGE_COLOR':
            return {
                ...state,
                color: action.color
            }
        default:
            returnstate; }}Copy the code

CreateStore evolution version

function createStore(reducer) {
    let state = {
        color: 'blue'
    }
    const getState = (a)= > state;
    // Rename changeState here to 'dispatch'
    const dispatch = (action) = > {
        Reducer receives the old state and action and returns a new state
        state = reducer(state, action);
    }
    return {
        getState,
        dispatch
    }
}
Copy the code

Different applications must have different states, and it would be unreasonable for us to define the value of state within createStore.

function createStore(reducer) {
    let state;
    const getState = (a)= > state;
    const dispatch = (action) = > {
        Reducer (state, action) returns a new state
        state = reducer(state, action);
    }
    return {
        getState,
        dispatch
    }
}
Copy the code

Note the definition of reducer. When an unrecognized action is encountered, the reducer directly returns the old state. Now, we use this to return the initial state.

If state has an initial state, it is very simple. We take the initial state value as the default value of the reducer parameters, and then send an action that the reducer does not understand in the createStore. The first time getState is called, the default value of the state is obtained.

CreateStore Evolution 2.0

function createStore(reducer) {
    let state;
    const getState = (a)= > state;
    // Whenever 'dispatch' has an action, we need to call 'reducer' to return a new state
    const dispatch = (action) = > {
        Reducer (state, action) returns a new state
        state = reducer(state, action);
    }
    // If you have an action whose type is' @@redux/__INIT__${math.random ()} ', I respect you for being a jerk
    dispatch({ type: `@@redux/__INIT__The ${Math.random()}` });

    return {
        getState,
        dispatch
    }
}
Copy the code

The createStore is now available everywhere, but do you think it would be silly to manually use renderApp() after every dispatch, which is currently called twice in the app, if you need to change state 1,000 times? Call renderApp() 1000 times manually?

Can we simplify this a little bit? RenderApp () is automatically called every time the data changes. Of course it is not possible to write renderApp() into the dispatch of createStore(), because in other applications the function name may not necessarily be renderApp(), and it may not only trigger renderApp(). Here you can introduce a publish-subscribe model to notify all subscribers when the status changes.

CreateStore Evolution 3.0

function createStore(reducer) {
    let state;
    let listeners = [];
    const getState = (a)= > state;
    //subscribe Returns an unsubscribe method each time it is called
    const subscribe = (ln) = > { 
        listeners.push(ln);
        // After subscribing, also allow unsubscribing.
        // Am I not allowed to unsubscribe from a magazine after I subscribe to it? Terrible ~
        const unsubscribe = (a)= > {
            listeners = listeners.filter(listener= >ln ! == listener); }return unsubscribe;
    };
    const dispatch = (action) = > {
        Reducer (state, action) returns a new state
        state = reducer(state, action);
        listeners.forEach(ln= > ln());
        
    }
    // If you have an action whose type is exactly the same as' @@redux/__INIT__${math.random ()} ', I respect you for being a jerk
    dispatch({ type: `@@redux/__INIT__The ${Math.random()}` });

    return {
        getState,
        dispatch,
        subscribe
    }
}
Copy the code

At this point, one of the simplest redux has been created, and createStore is the core of Redux. Let’s use this lite version of the story rewrite our code, index, js file content updates as follows (the to – story/SRC/index, js) :

function createStore() {
    //code(copy the above createStore code here yourself)
}
const initialState = {
    color: 'blue'
}

function reducer(state = initialState, action) {
    switch (action.type) {
        case 'CHANGE_COLOR':
            return {
                ...state,
                color: action.color
            }
        default:
            returnstate; }}const store = createStore(reducer);

function renderApp(state) {
    renderHeader(state);
    renderContent(state);
}
function renderHeader(state) {
    const header = document.getElementById('header');
    header.style.color = state.color;
}
function renderContent(state) {
    const content = document.getElementById('content');
    content.style.color = state.color;
}

document.getElementById('to-blue').onclick = function () {
    store.dispatch({
        type: 'CHANGE_COLOR'.color: 'rgb(0, 51, 254)'
    });
}
document.getElementById('to-pink').onclick = function () {
    store.dispatch({
        type: 'CHANGE_COLOR'.color: 'rgb(247, 109, 132)'
    });
}

renderApp(store.getState());
// Rerender every time state changes
store.subscribe((a)= > renderApp(store.getState()));
Copy the code

If we now want the font color to not be allowed to change after we click Pink, we can also unsubscribe:

const unsub = store.subscribe((a)= > renderApp(store.getState()));
document.getElementById('to-pink').onclick = function () {
    //code...
    unsub(); // Unsubscribe
}
Copy the code

A reducer, by the way, is a pure function that accepts the previous state and action and returns the new state. Don’t ask why the action must have a Type field, it’s just a convention (that’s how Redux is designed).

Legacy issues: WhyreducerBe sure to return a new onestate, rather than directly modifystate? Feel free to leave your answers in the comments section.

Having walked through redux’s core code step by step, let’s review the redux design philosophy:

ReduxDesign idea

  • ReduxThe entire application state (state) stored in a place (usually calledstore)
  • When we need to modify the status, we must send (dispatch) aaction( actionIt’s the one with thetypeField object)
  • Specialized state handlersreducerReceive the oldstateactionAnd a new one will be returnedstate
  • throughsubscribeSet up a subscription to notify all subscribers every time an action is sent.

We now have a basic version of Redux, but it doesn’t meet our needs. Our normal business development is not as simple as the example written above, and there is a problem: the Reducer function can be very long because there are many types of actions. This is definitely not conducive to code writing and reading.

Imagine that there are 100 actions that need to be dealt with in your business. If you compile these 100 actions in a Reducer, it will not only make people sick, but also the colleagues who maintain the code later want to kill people.

Therefore, we had better compile the Reducer separately and then merge the Reducer. Please welcome our combineReducers(to keep the same name as redux library)

combineReducers

First we need to make it clear that combineReducers is just a tool function that, as we said earlier, merges multiple reducers into one reducer. CombineReducers returns reducer, that is, it is a higher-order function.

As an example, redux doesn’t have to work with React, but since it works best with React, here’s the React code:

This time, in addition to the above demonstration, we added a counter function (refactoring with React ===> to-redux2):

// Now our state structure is as follows:
let state = {
    theme: {
        color: 'blue'
    },
    counter: {
        number: 0}}Copy the code

Obviously, modification subjects and counters can be divided and dealt with by different reducer is a better choice.

store/reducers/counter.js

Handles the state of the counter.

import { INCRENENT, DECREMENT } from '.. /action-types';

export default counter(state = {number: 0}, action) {
    switch (action.type) {
        case INCRENENT:
            return {
                ...state,
                number: state.number + action.number
            }
        case DECREMENT:
            return {
                ...state,
                number: state.number - action.number
            }
        default:
            returnstate; }}Copy the code

store/reducers/theme.js

Responsible for handling the state that changes the theme color.

import { CHANGE_COLOR } from '.. /action-types';

export default function theme(state = {color: 'blue'}, action) {
    switch (action.type) {
        case CHANGE_COLOR:
            return {
                ...state,
                color: action.color
            }
        default:
            returnstate; }}Copy the code

Each Reducer is responsible for managing only its share of the global state. State parameters of each Reducer are different, corresponding to the part of state data it manages.

import counter from './counter';
import theme from './theme';

export default function appReducer(state={}, action) {
    return {
        theme: theme(state.theme, action),
        counter: counter(state.counter, action)
    }
}
Copy the code

AppReducer is the reducer after the merger, but when there is a large reducer, it is tedious to write. Therefore, we write a tool function to generate such appReducer, and we name this tool function as combineReducers.

Let’s try writing the utility function combineReducers:

Ideas:

  1. combineReducersreturnreducer
  2. combineReducersThe input parameter of is multiplereducerObject of composition
  3. eachreducerOnly globalstateI’m responsible for
// Reducers is an object whose attribute values are the reducer of each split
export default function combineReducers(reducers) {
    return function combination(state={}, action) {
        The return value from the reducer is the new state
        let newState = {};
        for(var key in reducers) {
            newState[key] = reducers[key](state[key], action);
        }
        returnnewState; }}Copy the code

The Reducer will be responsible for returning the default value of state. In this example, dispatch({type:@@redux/INIT${math.random ()}}), The combination function returned by combineReducers(Reducers) is passed to createStore.

State =reducer(state,action), newstate. theme=theme(undefined, action), newstate. counter=counter(undefined, action), Reducer returns the initial values of newstate. theme and newstate. counter respectively.

Using this combineReducers can rewrite store/reducers/index.js

import counter from './counter';
import theme from './theme';
import { combineReducers } from '.. /redux';
// It is much simpler
export default combineReducers({
    counter,
    theme
});
Copy the code

The combineReducers we wrote seemed to meet our needs already, but it had a disadvantage that it returned a new state object each time, leading to meaningless re-rendering when the data did not change. Therefore, we can judge the data and return the original state when the data does not change.

CombineReducers evolution version

// Some judgment is omitted in the code, the default transfer of parameters are in line with the requirements, interested can view the source code for parameter validity judgment and processing
export default function combineReducers(reducers) {
    return function combination(state={}, action) {
        let nextState = {};
        let hasChanged = false; // Whether the state has changed
        for(let key in reducers) {
            const previousStateForKey = state[key];
            const nextStateForKey = reducers[key](previousStateForKey, action);
            nextState[key] = nextStateForKey;
            HasChanged is false only if all NextStateForKeys are equal to previousStateForKeyhasChanged = hasChanged || nextStateForKey ! == previousStateForKey; }// Return the original object if state has not changed
        returnhasChanged ? nextState : state; }}Copy the code

applyMiddleware

The documentation for applyMiddleware is pretty clear, and the following is a reference to the documentation:

logging

Consider a quick question: If we wanted to print state in the console every time the state changed, what would we do?

The simplest is:

/ /...
<button onClick={() => {
    console.log(store.getState());
    store.dispatch(actions.add(2)); > +}}</button>
/ /...
Copy the code

Of course, this is definitely not desirable, if we send it 100 times in code, we can’t write it 100 times. Since state is printed when state changes and state is printed before reducer call, reducer is called in dispatch, we can rewrite the store.dispatch method and print state before dispatch.

let store = createStore(reducer);
const next = store.dispatch; // The next command is consistent with the middleware source code
store.dispatch = action= > {
    console.log(store.getState());
    next(action);
}
Copy the code

The collapse of information

Suppose we not only need to print state, but also need to print an error message when we deliver an exception error.

const next = store.dispatch; // Next is named to be consistent with the middleware source code
store.dispatch = action= > {
    try{
        console.log(store.getState());
        next(action);
    } catct(err) {
        console.error(err); }}Copy the code

If we had other requirements, we would have had to constantly modify the store.dispatch method, which would have made this part of the code difficult to maintain.

Therefore, we need to separate loggerMiddleware from exceptionMiddleware.

let store = createStore(reducer);
const next = store.dispatch; // Next is named to be consistent with the middleware source code
const loggerMiddleware = action= > {
    console.log(store.getState());
    next(action);
}
const exceptionMiddleware = action= > {
    try{
        loggerMiddleware(action);
    }catch(err) {
        console.error(err);
    }
}
store.dispatch = exceptionMiddleware;
Copy the code

We know that a lot of middleware is provided by third parties, so dispatch and getState will definitely need to be passed as arguments to middleware.

const loggerMiddleware = ({dispatch, getState}) = > action => {
    const next = dispatch;
    console.log(getState());
    next(action);
}
const exceptionMiddleware = ({dispatch, getState}) = > action => {
    try{
        loggerMiddleware({dispatch, getState})(action);
    }catch(err) {
        console.error(err); }}/ / use
store.dispatch = exceptionMiddleware({dispatch, getState});
Copy the code

LoggerMiddleware in exceptionMiddleware is dead, which is definitely not reasonable. We want this to be a parameter so that we can use It flexibly. It doesn’t make sense that only exceptionMiddleware needs to be flexible. LoggerMiddleware, however, is further rewritten as follows:

const loggerMiddleware = ({dispatch, getState}) = > next => action= > {
    console.log(getState());
    return next(action);
}
const exceptionMiddleware = ({dispatch, getState}) = > next => action= > {
    try{
        return next(action);
    }catch(err) {
        console.error(err); }}/ / use
const next = store.dispatch;
const logger = loggerMiddleware({dispatch: store.dispatch, getState: store.getState});
store.dispatch = exceptionMiddleware(store)(logger(next));
Copy the code

We now have a generic middleware format.

The middleware receives a dispatch function from Next () and returns a dispatch function, which is used as the next middleware next().

But there is a small problem, when there is a lot of middleware, the code to use middleware can become cumbersome. To that end, Redux provides a utility function for applyMiddleware.

As you can see from the above, what we want to change is dispatch, so we need to rewrite store to return the store after the dispatch method is changed.

Therefore, we can make the following points clear:

  1. applyMiddlewareThe return value isstore
  2. applyMiddlewareDefinitely acceptmiddlewareAs a parameter
  3. applyMiddlewareTo accept{dispatch, getState}As an input parameter, howeverreduxThe source input parameter iscreateStorecreateStoreIf you think about it, there’s no need to create it externallystoreAfter all, it was created externallystoreIt doesn’t do anything except pass it in as an argument, so let’s put it increateStorecreateStoreThe parameters you need to use are passed in.
/ / applyMiddleWare returned to the store.
const applyMiddleware = middleware= > createStore => (. args) = > {
    letstore = createStore(... args);let middle = loggerMiddleware(store);
    let dispatch = middle(store.dispatch); // New dispatch method
    // Returns a new store-- overrides the dispatch method
    return {
        ...store,
        dispatch
    }
}
Copy the code

This is a case of middleware, but we know that middleware can be one or more, and that we’re dealing with more than one middleware problem.

/ / applyMiddleware returned to the store.
const applyMiddleware = (. middlewares) = > createStore => (. args) = > {
    letstore = createStore(... args);let dispatch;
    const middlewareAPI = {
        getState: store.getstate,
        dispatch: (. args) = >dispatch(... args) }// Pass the modified dispatch
    let middles = middlewares.map(middleware= > middleware(middlewareAPI));
    // Now we have multiple Middleware and need to augment dispatches multiple times
    dispatch = middles.reduceRight((prev, current) = > current(prev), store.dispatch);
    return {
        ...store,
        dispatch
    }
}
Copy the code

I do not know whether you understand the above middles.reduceRight, the following is a detailed explanation for you:

/* Three middleware */
let logger1 = ({dispatch, getState}) = > dispatch => action= > {
    console.log('111');
    dispatch(action);
    console.log('444');
}
let logger2 = ({ dispatch, getState }) = > dispatch => action= > {
    console.log('222');
    dispatch(action);
    console.log('555')}let logger3 = ({ dispatch, getState }) = > dispatch => action= > {
    console.log('333');
    dispatch(action);
    console.log('666');
}
let middle1 = logger1({ dispatch, getState });
let middle2 = logger2({ dispatch, getState });
let middle3 = logger3({ dispatch, getState });

//applyMiddleware(logger1,logger2,logger3)(createStore)(reducer)
// If you substitute directly
store.dispatch = middle1(middle2(middle3(store.dispatch)));
Copy the code

If we look at middle1(MIDDLE2 (middle3(store.dispatch)) above, if we think of middle1,middle2, and middle3 as each of the items in the array, we can think of reduce if we are familiar with the array API. If you are not familiar with Reduce, check out the MDN documentation.

//applyMiddleware(logger1,logger3,logger3)(createStore)(reducer)

//reduceRight execute from right to left
middles.reduceRight((prev, current) = > current(prev), store.dispatch);
Prev: store.dispatch current: middle3
Prev: middle3(store.dispatch) current: middle2
Prev: middle2(MIDDLE3 (store.dispatch)) current: middle1
/ / the result middle1 (middle2 (middle3 (store. Dispatch)))
Copy the code

For those of you who have read redux’s source code, you may know that the source code provides a compose function. Instead of using reduceRight, the compose function uses reduce, so the code is slightly different. But the analysis process is the same.

compose.js

export default function compose(. funcs) {
    // If there is no middleware
    if (funcs.length === 0) {
        return arg= > arg
    }
    // The length of the middleware is 1
    if (funcs.length === 1) {
        return funcs[0]}return funcs.reduce((prev, current) = >(... args) => prev(current(... args))); }Copy the code

As for the writing method of reduce, it is suggested to conduct an analysis like the above reduceRight

Rewrite applyMiddleware using the Compose tool function.

const applyMiddleware = (. middlewares) = > createStore => (. args) = > {
    letstore = createStore(... args);let dispatch;
    const middlewareAPI = {
        getState: store.getstate,
        dispatch: (. args) = >dispatch(... args) }let middles = middlewares.map(middleware= >middleware(middlewareAPI)); dispatch = compose(... middles)(store.dispatch);return {
        ...store,
        dispatch
    }
}
Copy the code

bindActionCreators

Redux also provides us with the bindActionCreators utility function, which is very simple in code and is rarely used directly in code, but will be used in react-Redux. Here, a brief explanation:

// Usually we write our actionCreator like this
import { INCRENENT, DECREMENT } from '.. /action-types';

const counter = {
    add(number) {
        return {
            type: INCRENENT,
            number
        }
    },
    minus(number) {
        return {
            type: DECREMENT,
            number
        }
    }
}

export default counter;
Copy the code

At the time of distribution, we need to write:

import counter from 'xx/xx';
import store from 'xx/xx';

store.dispatch(counter.add());
Copy the code

Of course, we could also write our actionCreator like this:

function add(number) {
    return {
        type: INCRENENT,
        number
    }
}
Copy the code

When distributed, you need to write:

store.dispatch(add(number));
Copy the code

One thing the above codes have in common is that they all send an action to store.dispatch. So consider writing a function that binds Store. dispatch to actionCreator.

function bindActionCreator(actionCreator, dispatch) {
    return  (. args) = >dispatch(actionCreator(... args)); }function bindActionCreators(actionCreator, dispatch) {
    //actionCreators can be a normal function or an object
    if(typeof actionCreator === 'function') {
        // If it is a function, returns a function called dispatch
        bindActionCreator(actionCreator, dispatch);
    }else if(typeof actionCreator === 'object') {
        // If it is an object, then each item of the object should return bindActionCreator
        let boundActionCreators = {}
        for(let key in actionCreator) {
            boundActionCreators[key] = bindActionCreator(actionCreator[key], dispatch);
        }
        returnboundActionCreators; }}Copy the code

In use:

let counter = bindActionCreators(counter, store.dispatch);
/ / distribution
counter.add(number);
counter.minus(number);
Copy the code

This doesn’t look like much simplification, but we’ll explain why we need this utility function later when we look at React-Redux.

At this point, our Redux is almost complete. The replaceReducer method is provided by createStore, and the second and third parameters of the createStore are not mentioned. We will not expend them here.

Refer to the link

  1. The React. Js little book
  2. Redux Chinese document
  3. Fully understand REdux (achieving a REdux from zero)

Pay attention to the public number, join the technical exchange group