This article explains and implements all the Redux apis, including createStore, combineReducers, applyMiddleware, compose, bindactioncreators. All implementations remove a lot of unnecessary logic for simplicity, but keep the main functionality intact. The source code for this article can be viewed here.

createStore

Example:

export function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
};
const { dispatch, subscribe, getState } = createStore(counterReducer);
const unsubscribe = subscribe(() = > console.log(getState()));
dispatch({ type: 'INCREMENT' });
unsubscribe();
Copy the code

Implementation:

Here itself is not difficult to repeat, look at the following 👇 code. CreateStore returns the store, which contains the following four methods.

  • getState(): Returns the current state of the application.
  • dispatch(action): Distributes the action, which is the only way to trigger a state change.
  • subscribe(listener): Adds a change listener.
  • replaceReducer(nextReducer): Replaces store the reducer that is currently used to calculate state.
// Ensure that the name is not the same as the user-defined actionType
const ACTION_TYPE_INIT = `@@redux/INITThe ${Math.random()}`;
const ACTION_TYPE_REPLACE = `@@redux/REPLACEThe ${Math.random()}`;

function createStore(reducer, preloadedState) {
  let currentReducer = reducer;
  const listeners = [];
  let state = preloadedState;

  const getState = () = > {
    return state;
  };

  const subscribe = listener= > {
    listeners.push(listener);
    // Used to unsubscribe
    const unsubscribe = () = > {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
    return unsubscribe;
  }

  const dispatch = action= > {
    state = currentReducer(state, action);
    listeners.forEach(listener= > listener());
  };

  function replaceReducer(nextReducer) {
    if (typeofnextReducer ! = ='function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.REPLACE } ); // Replace the reducer and reinitialize the state
    return store;
  }
  // The reducer has the default state set. Call dispatch to initialize the state
  dispatch({ type: ACTION_TYPE_INIT });

  const store = { dispatch, subscribe, getState, replaceReducer };
  return store;
};
Copy the code

combineReducers(reducers)

If the application is large, divide the reducer into multiple reducer functions so that each reducer manages part of the state independently. CombineReducers allows you to combine multiple reducer functions that have been split into one final reducer function for the createStore.

Example:

// counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}
// todos.js
export default function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

const rootReducer = combineReducers({ todoList: todos, count: counter });
const store = createStore(rootReducer);
store.getState(); // { todoList: [], count: 0 }
Copy the code

Implementation:

The idea is to return the reducer function (combination) after the combination. When this combination function receives the action, it calls all the reducer functions that are split and gets the latest state (nextState).

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);

  return function combination(state = {}, action) {
    const nextState = {};
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i];
      const reducer = reducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      nextState[key] = nextStateForKey;
    }
    return nextState;
  };
};
Copy the code

applyMiddleware(... middleware)

1. Middleware

The nature of the Redux middleware extends the functionality of Dispatches and returns enhanced dispatches. Let’s first modify the Dispatch method in the createStore to see the middleware running order.

const dispatch = action= > {
  console.log('The original Dispatch is working')
  state = reducer(state, action);
  listeners.forEach(listener= > listener());
  console.log('The original Dispatch is done.')};Copy the code

First, customize two middleware and use:

function middleware1 (store) {
  return next= > action= > {
    console.log('The enhanced Dispatch of Middleware 1 is working.');
    const returnValue = next(action);
    console.log('The enhanced Dispatch of Middleware 1 is done.');
    return returnValue;
  };
}
function middleware2 (store) {
  return next= > action= > {
    console.log('The enhanced Dispatch of Middleware 2 is working.');
    const returnValue = next(action);
    console.log('The enhanced Dispatch of Middleware 2 is done.');
    return returnValue;
  };
}
const store = createStore(todosReducer, {}, applyMiddleware(middleware1, middleware2));
Copy the code

When we launch a store.Dispatch (action), we can see the following log:

The enhanced Middleware 1 dispatch starts working and the enhanced Middleware 2 dispatch starts working and the original Dispatch starts working and the original Dispatch ends up working and the enhanced Middleware 2 dispatch starts working The Middleware 1 enhanced Dispatch is doneCopy the code

It can be seen that in the case of multiple middleware, next is the dispatch added by the next middleware, and even the original dispatch called by the last middleware. The original dispatch is augmented by middleware from right to left.

So in the case of only one middleware, next is the original Store.Dispatch. So the above code is equivalent to:

const store = createStore(todosReducer, {});
const next = store.dispatch;

store.dispatch = action= > {
  console.log('The enhanced Dispatch of Middleware 1 is working.');
  next(action);
  console.log('The enhanced Dispatch of Middleware 1 is done.');
}
Copy the code

If there are two middleware:

const store = createStore(todosReducer, {});
const next = store.dispatch;

const next2 = action= > {
  console.log('The enhanced Dispatch of Middleware 2 is working.');
  next(action); // The original store.dispatch
  console.log('The enhanced Dispatch of Middleware 2 is done.');
}

store.dispatch = action= > {
  console.log('The enhanced Dispatch of Middleware 1 is working.');
  next2(action);
  console.log('The enhanced Dispatch of Middleware 1 is done.');
}
Copy the code

Although the above implementation of custom intermediate functions, but not flexible, because there may be more than two middleware, order needs to be free exchange. So we need to add a parameter to support passing dispatchings enhanced by the next middleware:

const next = store.dispatch;

const middleware2 = next= > action= > {
  console.log('The enhanced Dispatch of Middleware 2 is working.');
  const returnValue next(action);
  console.log('The enhanced Dispatch of Middleware 2 is done.');
  return returnValue;
}

const middleware1 = next= > action= > {
  console.log('The enhanced Dispatch of Middleware 1 is working.');
  const returnValue = next(action);
  console.log('The enhanced Dispatch of Middleware 1 is done.');
  return returnValue;
}

store.dispatch = middleware1(middleware2(next));
Copy the code

Consider that each intermediate internal does more than just execute the next next. For example, a Logger middleware needs to obtain the state via the store.getState method.

const logger = next= > action= > {
  return next= > action= > {
    console.log('About to Dispatch:', action)
    const returnValue = next(action)
    console.log('State after dispatch:', store.getState())
    return returnValue
  }
}
Copy the code

These are all from the Store, so each middleware needs to add a parameter, mAPI, to receive these apis.

const next = store.dispatch;

const middleware2 = mAPI= > next= > action= > {
  console.log('The enhanced Dispatch of Middleware 2 is working.');
  const returnValue = next(action);
  console.log('The enhanced Dispatch of Middleware 2 is done.');
  return returnValue;
}

const middleware1 = mAPI= > next= > action= > {
  console.log('The enhanced Dispatch of Middleware 1 is working.');
  const returnValue = next(action);
  console.log('The enhanced Dispatch of Middleware 1 is done.');
  return returnValue;
}
const mAPI = store;
const m2 = middleware2(mAPI);
const m1 = middleware1(mAPI);

store.dispatch = m1(m2(next));
Copy the code

Redux states that the only apis available in the middleware are getState and Dispatch. So after the improvement:

const next = store.dispatch;
// ...

let dispatch = next;
const mAPI = {
  getState: store.getState,
  dispatch: (action, ... args) = >dispatch(action, ... args) }const chain = [middleware1, middleware2].map(m= > m(mAPI)); // [m1, m2]

// Implement: store.dispatch = m1(m2(next))
dispatch = chain.reverse().forEach(m= > {
  dispatch = m(dispatch);
});
store.dispatch = dispatch;
Copy the code

applyMiddleware

The middleware functionality has been implemented, but it’s not quite elegant enough. The next step is to fold the process into an applyMiddleware function.

conststore = createStore(reducer, preloadedState, applyMiddleware(... middlewares));Copy the code

applyMiddleware(… Middlewares) is designed to enhance the Dispatch method returned by the createStore. So applyMiddleware(… Middlewares) returns the enhancer of the createStore. Passing the createStore into the enhancer returns the upgraded version of the createStore.

constenhancer = applyMiddleware(... middlewares);const createStore2 = enhancer(createStore);
Copy the code

Add logic to createStore that returns enhanced createStore when passed to enhancer:

// const enhancer = applyMiddleware(middleware1, middleware2);
function createStore(reducer, preloadedState, enhancer) {
  if (typeof applyMiddleware === 'function') {
    const createStore2 = enhancer(createStore);
    return createStore2(reducer, preloadedState);
  }
  // ...
}
Copy the code

implementationapplyMiddleware :

Fold the processing middleware and dispatch logic into the createStore enhancer returned by applyMiddleware.

function applyMiddleware(. middlewares) {
  const enhancer = createStore= > (reducer, preloadedState) = > {
      const store = createStore(reducer, preloadedState);

      const next = store.dispatch;
      let dispatch = next;

      const mAPI = {
        getState: store.getState,
        dispatch: (action, ... args) = >dispatch(action, ... args), }const chain = middlewares.map(middleware= > middleware(mAPI));

      chain.reverse().forEach(m= > {
        dispatch = m(dispatch);
      });

      return {
        ...store,
        dispatch
      }
    };
  return enhancer;
}
Copy the code

compose(... functions)

For example, compose is used to compose the received function from right to left into a final function. The return value of the function on the right is given as an argument to the function on the left. For example, compose(funcA(funcB(funcC()))))).

function compose(. funcs) {
  if (funcs.length === 0) return arg= > arg;

  if (funcs.length === 1) return funcs[0];

  return funcs.reduce((a, b) = >{ retrun (... args) => a(b(... args)); }); }Copy the code

Store. Dispatch = m1(m2(store.dispatch)), so applyMiddleware can be used for compose:

function applyMiddleware(. middlewares) {
  const enhancer = createStore= > (reducer, preloadedState) = > {
      const store = createStore(reducer, preloadedState);

      let dispatch;

      const mAPI = {
        getState: store.getState,
        dispatch: (action, ... args) = >dispatch(action, ... args), }const chain = middlewares.map(middleware= >middleware(mAPI)); dispatch = compose(... chain)(store.dispatch);return {
        ...store,
        dispatch
      }
    };
  return enhancer;
}
Copy the code

bindActionCreators(actionCreators, dispatch)

ActionCreator is the function that creates the action, such as the two 👇 below. The advantage of actionCreator is that you reuse the logic that created the Action, and when you use dispatch you only need to dispatch(actionCreator(value)).

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}
Copy the code

The only case with bindActionCreators is when you need to pass the actionCreator to the child, but don’t want the child to feel the Redux.

import { addTodo, removeTodo } from './TodoActionCreators'

class TodoListContainer extends Component {
  constructor(props) {
    super(props)
    const { dispatch } = props; // Dispatches injected by React-Redux

    // Bind the Dispatch method to actionCreator
    this.boundActionCreators = bindActionCreators({
      addTodo,
      removeTodo,
    }, dispatch);
    console.log(this.boundActionCreators); // { addTodo: Function, removeTodo: Function }
  }

  render() {
    // Todos injected by React-redux:
    let { todos } = this.props
    return <TodoList todos={todos} {. this.boundActionCreators} / >

    // Do not use bindActionCreators:
    // Pass Dispatch directly as a prop to the child component, but the child component needs to introduce actionCreator
    // return <TodoList todos={todos} dispatch={dispatch} />;}}Copy the code

implementationbindActionCreators :

BindActionCreators’ implementation is simple, if we don’t want to be aware of the dispatch and actionCreator, just use closures to hide:

function bindActionCreator(actionCreator, dispatch) {
  return function (this. args) {
    return dispatch(actionCreator.apply(this, args)); }}/ / use:
const addTodo = bindActionCreator(addTodo, store.dispatch);
addTodo('Use Redux');
Copy the code

Return a boundActionCreators object if there are multiple ActionCreators:

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeofactionCreators ! = ='object' || actionCreators === null) {
    throw new Error('bindActionCreators expected an object or a function.');
  }

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];
    if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch); }}return boundActionCreators;
}

/ / use:
const boundActionCreators = bindActionCreator({
  addTodo,
  removeTodo,
}, dispatch);
boundActionCreators.addTodo('Use Redux');
Copy the code

reference

  • redux
  • redux github