The realization of the story

This article implements a simple redux and React-redux from zero

  1. reduxThe design idea and realization principle of the
  2. Redux middlewareThe design idea and realization principle of the
  3. react-reduxThe design idea and realization principle of the

Redux is a state manager that stores data. For example, when we create store.js, we store the data in this file. We simply reference this file anywhere to get the corresponding state value:

let state = {
  count: 1
}
Copy the code

We read and modify the following states:

console.log(state.count)
state.count = 2
Copy the code

Now we have state (count) modification and use! There is, of course, an obvious problem:

  1. The state manager can only managecount, not universal.
  2. Modify thecountAfter that, usecountCan not receive notification.

To realize the subscribe

We can solve this problem using a publish-subscribe model. Let’s encapsulate this redux with a function

function createStore(initState) {
  let state = initState
  let listeners = []

  /* Subscribe function */
  function subscribe(listener) {
    listeners.push(listener)
  }

  function changeState(newState) {
    state = newState
    /* Execute notification */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  function getState() {
    return state
  }

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

At this point we have completed a simple state manager.

  1. stateThe data can be determined freely
  2. We modify the state and listen for changes where we subscribe.
let initState = {
  count: 1.info: {
    age: 18}}let store = createStore(initState)

store.subscribe((a)= > {
  let state = store.getState()
  console.log('subscribe function one: ', state)
})
store.subscribe((a)= > {
  let state = store.getState()
  console.log('subscribe function two: ', state) }) store.changeState({ ... store.getState(),count: store.getState().count + 1}) store.changeState({ ... store.getState(),info: { age: store.getState().info.age - 1}})// ==== result
// subscribe function one: { count: 2, info: { age: 18 } }
// subscribe function two: { count: 2, info: { age: 18 } }
// subscribe function one: { count: 2, info: { age: 17 } }
// subscribe function two: { count: 2, info: { age: 17 } }
Copy the code

The important thing to understand here is that createStore provides changeState, getState, and Subscribe capabilities.

In the above function, we call store.changeState to change the value of state, which is a big disadvantage. Such as store. ChangeState ({})

If we are not careful, we will clear the data of the store or modify the data of other components by mistake, which is obviously not safe, and it is difficult to troubleshoot errors. Therefore, we need to operate the store conditionally to prevent users from directly modifying the data of the store.

Therefore, we need a constraint to modify the value of state without allowing unexpected circumstances to empty or mismanipulate the value of state. We can solve this problem in two steps:

  1. dispatch: Make a planstateModify the plan, tellstoreWhat is my revision plan?
  2. reducer: modifystore.changeStateMethod to tell it to modifystateAccording to our plan.

In other words, we changed store.changeState to store.dispatch and passed an additional reducer function in the function to constrain the modification of the state value.

Realize the reducer

Reducer is a pure function that accepts a state and returns a new state.

function createStore(reducer, initState) {
  let state = initState
  let listeners = []

  /* Subscribe function */
  function subscribe(listener) {
    listeners.push(listener)
  }

  /* Change the state value */
  function dispatch(action) {
    state = reducer(state, action)
    /* Execute notification */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  function getState() {
    return state
  }

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

Let’s try to use Dispatch and reducer to realize auto-increase and auto-decrease

let initState = {
  count: 1.info: {
    age: 18}}function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    default:
      return state
  }
}

let store = createStore(reducer, initState)

store.subscribe((a)= > {
  let state = store.getState()
  console.log('subscribe function: ', state)
})

store.dispatch({ type: 'INCREMENT' }) / / since the increase
store.dispatch({ type: 'DECREMENT' }) / / the decrement
store.dispatch({ count: 2 }) // Unplanned: does not take effect
Copy the code

We know that reducer is a constraint function that receives the old state and returns the new state as planned. So in our project, we have a lot of states, and each state needs a constraint function. What would it look like to write all of them together?

All plans are written in a reducer function, which makes the Reducer function extremely large and complex. The following will encapsulate combineReducers to granulate the Reducer function.

Implement combineReducers

A granular reducer

As a rule of thumb, we have to split a number of reducer functions by component dimension and then combine them with a reducer function.

Let’s manage two states, one counter and one info.

let state = {
  counter: { count: 0 },
  info: { age: 18}}Copy the code

Their respective reducer

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 }
    default:
      return state
  }
}

function infoReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT-AGE':
      return { age: state.age + 1 }
    default:
      return state
  }
}
Copy the code

We try to implement the combineReducers function

  1. Passing in object parameters,keyValue is thestateState of the treekeyValue,valueFor the correspondingreducerFunction.
  2. Iterate over the object parameters, executing each onereducerFunction, passed instate[key], the function gets eachreducerThe lateststateValue.
  3. couplingstateAnd returns the value of. Returns the merged newreducerFunction.
function combineReducers(reducers) {
  /* reducerKeys = ['counter', 'info']*/
  const reducerKeys = Object.keys(reducers)

  /* Returns the new reducer function */ after the merge
  return function combination(state = {}, action) {
    /* Generates a new state*/
    const nextState = {}

    /* Iterate through all the reducers to consolidate into a new state*/
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      /* State of the previous key */
      const previousStateForKey = state[key]
      /* Execute reducer to obtain the new state*/
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState
  }
}
Copy the code

Using combineReducers:

const reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
})

let store = createStore(reducers, initState)

store.subscribe((a)= > {
  let state = store.getState()
  console.log('subscribe function: ', state)
})

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT-AGE' })
Copy the code

However, this is not enough. We split reducer according to component dimension and merge it through combineReducers. But there is another problem, we still write state together, which makes the state tree very large, unintuitive, and difficult to maintain. We need to split, a state and a reducer write piece.

A granular state

CreateStore ({type: Symbol()}) combineReducers

function createStore(reducer, initState) {
  let state = initState
  let listeners = []

  /* Subscribe function */
  function subscribe(listener) {
    listeners.push(listener)
  }

  function dispatch(action) {
    state = reducer(state, action)
    /* Execute notification */
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  }

  /* Attention!! This is the only change to get the initial value */ with a type that does not match any of the plans
  dispatch({ type: Symbol()})function getState() {
    return state
  }

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

Pass state into their respective reducer:

function counterReducer(state = { count: 1 }, action) {
  / /...
}

function infoReducer(state = { age: 18 }, action) {
  / /...
}

/ / merge reducer
const reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
})

/ / remove initState
let store = createStore(reducers)

console.log(store.getState()) // { counter: { count: 1 }, info: { age: 18 } }
Copy the code

Let’s think about what this line can do.

  1. createStoreWhen using one does not match anytypeactionTo triggerstate = reducer(state, action)
  2. becauseaction.typeIt doesn’t match each childreducerWill came into thedefaultItem that returns its own initializationstate, so you get the initializedstateThe tree.

Implementation of Redux middleware

If you have used server-side libraries such as Express and Koa, you are probably already familiar with the concept of middleware.

The so-called middleware can be understood as the interceptor, which is used for intercepting and processing certain processes, and the middleware can be used in series. This is an extension, or rewrite, of Dispatch to enhance its functionality.

Middleware Examples

Redux-logger: Redux-Logger: Redux-Logger: Redux-Logger: Redux-Logger: Redux-Logger:

const reducers = combineReducers({ counter: counterReducer })

let store = createStore(reducers)
const next = store.dispatch

/ / rewrite dispatch
store.dispatch = action= > {
  console.log('prevState: ', store.getState())
  console.log('action', action)
  next(action)
  console.log('nextState: ', store.getState())
}

store.dispatch({ type: 'INCREMENT' })
Copy the code

The output

prevState:  { counter: { count: 1 } }
action { type: 'INCREMENT' }
nextState:  { counter: { count: 2 } }
Copy the code

We have now implemented a simple redux-Logger middleware. I have another requirement to record the reason for every data error. Let’s expand dispatch

store.dispatch = action= > {
  try {
    next(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}
Copy the code

This way, every time a dispatch exception occurs, we record it.

Multi-middleware collaboration

I now need to log both exceptions and logs. What do I do? Of course it is very simple, the two functions combined!

store.dispatch = action= > {
  try {
    console.log('prevState: ', store.getState())
    console.log('action', action)
    next(action)
    console.log('nextState: ', store.getState())
  } catch (err) {
    console.error('Error Report:', err)
  }
}
Copy the code

What if another requirement comes along? And then the dispatch function? What about 10 more needs? The dispatch function will be too big and messy to maintain!

We need to consider how to achieve highly scalable multi-middleware cooperation mode.

  1. We put theloggerMiddlewareextracted
const loggerMiddleware = action= > {
  console.log('prevState: ', store.getState())
  console.log('action', action)
  next(action)
  console.log('nextState: ', store.getState())
}
Copy the code
  1. We put theexceptionMiddlewareextracted
const exceptionMiddleware = action= > {
  try {
    /*next(action)*/
    loggerMiddleware(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}
store.dispatch = exceptionMiddleware
Copy the code
  1. There is a serious problem with the current codeexceptionMiddlewareIt’s dead in there.loggerMiddlewareWe need to letnext(action)Make it dynamic, any middleware will do
const exceptionMiddleware = next= > action => {
  try {
    /*loggerMiddleware(action); * /
    next(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}
/*loggerMiddleware becomes a parameter */
store.dispatch = exceptionMiddleware(loggerMiddleware)
Copy the code
  1. In the same way,loggerMiddlewareThe inside of thenextNow identity is equal to thetastore.dispatch, resulting inloggerMiddlewareThere is no way to extend other middleware! We also put thenextLet me write it as dynamic
const loggerMiddleware = next= > action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}
Copy the code

So far, we have explored a highly scalable middleware cooperation model!

const store = createStore(reducer)
const next = store.dispatch

const loggerMiddleware = next= > action => {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = next= > action => {
  try {
    next(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next))
Copy the code

We happily created loggermiddleware.js, an exceptionMiddleware.js file, to separate the two middleware pieces into a separate file. Will there be any problems?

LoggerMiddleware contains an external variable store, which makes it impossible to isolate middleware. Let’s pass store as an argument

const store = createStore(reducer)
const next = store.dispatch

const loggerMiddleware = store= > next => action= > {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = store= > next => action= > {
  try {
    next(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}

const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(logger(next))
Copy the code

At this point, we’ve actually implemented two separate middleware pieces!

Now I have a requirement to print the current timestamp before printing the log. Use middleware!

const timeMiddleware = store= > next => action= > {
  console.log('time'.new Date().getTime())
  next(action)
}

const time = timeMiddleware(store)
store.dispatch = exception(time(logger(next)))
Copy the code

Implement applyMiddleware

In the last section we fully implemented the right middleware! But the way middleware is used is not very friendly

let store = createStore(reducers)
const next = store.dispatch

const loggerMiddleware = store= > next => action= > {
  console.log('this state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = store= > next => action= > {
  try {
    next(action)
  } catch (err) {
    console.error('Error Report:', err)
  }
}

const timeMiddleware = store= > next => action= > {
  console.log('time'.new Date().getTime())
  next(action)
}

const time = timeMiddleware(store)
const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(time(logger(next)))
Copy the code

In fact, we only need to know three middleware, the rest of the details can be encapsulated! We do this by extending createStore!

Let’s look at the use of expectations

/* Receives the old createStore and returns the new createStore*/
const newCreateStore = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)(createStore)

/* Returns a store whose dispatch has been overwritten */
const store = newCreateStore(reducer)
Copy the code

Implement applyMiddleware

const applyMiddleware = function(. middlewares) {
  /* Returns a method to override createStore */
  return function rewriteCreateStoreFunc(oldCreateStore) {
    /* Returns the new createStore*/ after rewriting
    return function newCreateStore(reducer, initState) {
      / * 1. Generate store * /
      const store = oldCreateStore(reducer, initState)
      /* Pass store to each middleware, equivalent to const logger = loggerMiddleware(store); * /
      /* const chain = [exception, time, logger]*/
      const chain = middlewares.map(middleware= > middleware(store))
      let dispatch = store.dispatch
      / * implementation exception (time ((logger (dispatch)))) * /
      chain.reverse().map(middleware= > {
        dispatch = middleware(dispatch)
      })

      / * 2. Rewrite the dispatch * /
      store.dispatch = dispatch
      return store
    }
  }
}
Copy the code

Now there’s a small problem. We have two createStore types.

/* createStore*/ without middleware
let store = createStore(reducers, initState)

/* createStore*/ with middleware
const rewriteCreateStoreFunc = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)
const newCreateStore = rewriteCreateStoreFunc(createStore)
const store = newCreateStore(reducer, initState)
Copy the code

To make it more uniform for users, we can easily make them consistent by modifying the createStore method

function createStore(reducer, initState, rewriteCreateStoreFunc) {
  /* If there is rewriteCreateStoreFunc, use the new createStore */
  if (rewriteCreateStoreFunc) {
    const newCreateStore = rewriteCreateStoreFunc(createStore)
    return newCreateStore(reducer, initState)
  }
  /* Otherwise, follow the normal process */
  / /...
}
Copy the code

Final usage

const rewriteCreateStoreFunc = applyMiddleware(
  exceptionMiddleware,
  timeMiddleware,
  loggerMiddleware
)
const store = createStore(reducer, initState, rewriteCreateStoreFunc)
Copy the code

compose

In our applyMiddleware, we convert [A, B, C] to A(B(C(next))

const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map(middleware= > {
  dispatch = middleware(dispatch)
})
Copy the code

Redux provides a compose method that does this for us

constchain = [A, B, C] dispatch = compose(... chain)(store.dispatch)Copy the code

How does he do that

export default function compose(. funcs) {
  if (funcs.length === 1) {
    return funcs[0]}return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code

The compose function may be a bit tricky for newcomers to understand, but you just need to know what it does.

Omit initState

Sometimes when we create a store we don’t pass initState, how do we use it?

const store = createStore(reducer, {}, rewriteCreateStoreFunc)
Copy the code

Redux allows us to write it this way

const store = createStore(reducer, rewriteCreateStoreFunc)
Copy the code

We just need to change the createStore function. If the second argument is an object, we call it initState, and if it is function, we call it rewriteCreateStoreFunc.

function craeteStore(reducer, initState, rewriteCreateStoreFunc) {
  if (typeof initState === 'function') {
    rewriteCreateStoreFunc = initState
    initState = undefined
  }
  / /...
}
Copy the code

The realization of the react – story

In the previous section, we completed a simple Redux. If a component wants to access public state from store, it needs to perform four steps: import to store, getState to obtain state, Dispatch to modify state, subscribe to update, and the code is relatively redundant.

React-redux provides a way to merge operations: React-redux provides a Provider and a connect API. Provider puts a store into this. Context without importing it. Connect merges getState and Dispatch into this.props and automatically subscribes to updates, simplifying the other three steps,

To realize the Provider

import React from 'react'
import PropTypes from 'prop-types'

export default class Provider extends React.Component {
  // Static childContextTypes is declared to specify the properties of the context object
  static childContextTypes = {
    store: PropTypes.object
  }

  // Implement the getChildContext method, which returns the context object, also fixed
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  // Render the provider-wrapped component
  render() {
    return this.props.children
  }
}
Copy the code

Once the Provider is complete, we can fetch store from the component in the form this.context.store, without importing store separately.

To realize the connect

Let’s think about how to implement CONNECT. Let’s review how to use Connect:

connect(mapStateToProps, mapDispatchToProps)(App)
Copy the code

We already know that connect takes two methods, mapStateToProps and mapDispatchToProps, and returns a higher-order function that takes a component, / / connect/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import bindActionCreators from '.. /redux/bindActionCreators'

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function(Component) {
    class Connect extends React.Component {
      componentDidMount() {
        // Get the store from context and subscribe to updates
        this.context.store.subscribe(this.handleStoreChange.bind(this))
      }
      handleStoreChange() {
        // There are several ways to trigger the update. For brevity, forceUpdate forces the update directly. Readers can also trigger child component updates via setState
        this.forceUpdate()
      }

      render() {
        const dispathProps =
          typeof mapDispatchToProps &&
          bindActionCreators(mapDispatchToProps, this.context.store.dispatch)

        return (
          <Component// Passed to the componentpropsTo be,connectThis higher-order component returns the original component as is.. this.props} / / according tomapStateToPropsthestatehangthis.props{on. mapStateToProps(this.context.store.getState} / / according to ())mapDispatchToPropsthedispatch(action) onthis.props{on. dispathProps} / >ContextTypes = {store: proptypes. object} return Connect}}Copy the code

At the end of the article

See the code for implementing redux and React-redux

  • Derived from a complete understanding of REdux (implementing a REdux from zero
  • See the Redux principle in 10 lines of code