The outline

This article uses React to implement a three line counter four ways to write

  • longhand
  • MVC writing
  • Flux of writing
  • Story writing

In the process of analyzing the corresponding problems, in order to comb MVC, Flux, Redux context, attached

  • Flux source analysis
  • Redux source analysis

To enhance understanding

And all of this article can be found in the Flux-Redux-Demo repository.

longhand

See: 0. Normal

// counter
export default class extends React.Component {
  render() {
    return <li>
      <button onClick={()= > this.props.onCounterUpdate('minus')}>-</button>
      <button onClick={()= > this.props.onCounterUpdate('plus')}>+</button>
      {this.props.caption} Count: {this.props.value}
    </li>}}// controlpanel
export default class ControlPanel extends React.Component {
  state = {
    nums: [0.0.0],
  }
  onCounterUpdate = (type, index) = > {
    const { nums } = this.state
    const newNums = [...nums]
    if (type === 'minus') {
      if (nums[index] > 0) {
        newNums[index] = newNums[index] - 1}}else {
      newNums[index] = newNums[index] + 1
    }

    this.setState({ nums: newNums })
  }
  render() {
    const { nums } = this.state
    return (
      <div>I am writing:<ul>
          {
            nums.map((num, index) => {
              return <Counter value={num} caption={index} key={index} onCounterUpdate={(type)= > this.onCounterUpdate(type, index)} />
            })
          }
        </ul>{nums.reduce((memo, n) => memo + n, 0)}</div>)}}Copy the code

As you can see, this is actually a pretty good way to write it if you’re just talking about a counting component like this.

However,

  1. This section is also needed elsewhere (for example, a component of the same level, a sibling of a parent component)numsWe need to change this as wellnumsWhat about the data?
  2. Based on the first problem, we can only nest the data layer by layer. We can put this part of data layer by layer into higher/higher/higher levels… Management, and then layers of props down events up
  3. The scenario of problem 2, should belong to the biggest problem, single and a person playing, tired, the key will be more difficult to maintain in the future, especially more components depend on thisnumsWhen it comes to data

Then I thought of using MVC/PubSub to do it, put the data in a separate place for maintenance, every data update through the form of PubSub, listening to the data changes, and then set to the component, rendering View layer

MVC writing

See: 1. The MVC

// model
export default [0.0.0]

// controller
import nums from './model'
const eventStack = {}
const pubsub = {
  on(key, handler) {
    eventStack[key] = handler
  },
  emit(key) {
    eventStack[key](nums[key])
  }
}
export default{ listen(... params) { pubsub.on(... params) }, update(index, count) { nums[index] = count pubsub.emit(index) pubsub.emit('all')
  },
  getNum(index) {
    return nums[index]
  },
  getNums() {
    return nums
  }
}

// counter
export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      num: this.getNum()
    }
  }
  onCounterUpdate = (type) = > {
    const { num } = this.state
    if (type === 'minus') {
      if (num > 0) {
        controller.update(this.props.caption, num - 1)}}else {
      controller.update(this.props.caption, num + 1)
    }
  }
  componentDidMount() {
    controller.listen(this.props.caption, () => {
      this.setState({ num: this.getNum() })
    })
  }
  getNum = (a)= > {
    return controller.getNum(this.props.caption)
  }
  render() {
    return <li>
      <button onClick={()= > this.onCounterUpdate('minus')}>-</button>
      <button onClick={()= > this.onCounterUpdate('plus')}>+</button>
      {this.props.caption} Count: {this.state.num}
    </li>}}// total
export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      total: this.getTotal()
    }
  }
  componentDidMount() {
    controller.listen('all', () = > {this.setState({ total: this.getTotal() })
    })
  }
  getTotal = (a)= > {
    return controller.getNums().reduce((memo, n) = > memo + n, 0)
  }
  render() {
    return <div>{this.state.total} {this.state.total}</div>}}// controlpanel
export default class ControlPanel extends React.Component {
  render() {
    return (
      <div>MVC:<div>
          <div>
            <ul>
              {
                [0, 1, 2].map((item) => {
                  return <Counter caption={item} key={item} />})}</ul>
          </div>
        </div>
        <Total />
      </div>)}}Copy the code

As you can see above, the pattern of MVC PubSub shares data sources. Now the data is managed in one place, so that neither the props nor the props components are passed through the layers

This brings other problems:

  1. Pubsub, MVC needs their own implementation. It is easy to write something like pubsub.emit(‘all’), which is difficult to maintain (therefore, the team also needs a dedicated PubSub, MVC implementation, and specification definition).

  2. More critical: In order to accommodate view updates, both the ControlPanel and counter should be manually monitored for updates in the business layer, and state should be set separately (i.e. Before Flux, Backbone is used to update trigger data, and event listening is performed on componentDidMount, which is similar to the above concept

  3. If you need more data, it’s going to look like this

    controller.listen(a, () => {
      this.setState({ a: this.getA() })
    })
    controller.listen(b, () => {
      this.setState({ b: this.getB() })
    })
    controller.listen(c, () => {
      this.setState({ c: this.getC() })
    })
    controller.listen(d), () => {
      this.setState({ d: this.getD() })
    })
    Copy the code

So what can we do to avoid the 1, 2, and 3 issues (what can we do to encapsulate the specification, encapsulate the data binding injection?)

So we came to Flux

Flux of writing

See: 2. Flux

// NumsActionTypes
const ActionTypes = {
  INCREASE_COUNT: 'INCREASE_COUNT'.DECREASE_COUNT: 'DECREASE_COUNT'};export default ActionTypes;

// NumsAction
const Actions = {
  increaseCount(index) {
    NumsDispatcher.dispatch({
      type: NumsActionTypes.INCREASE_COUNT,
      index,
    });
  },
  decreaseCount(index) {
    NumsDispatcher.dispatch({
      type: NumsActionTypes.DECREASE_COUNT, index, }); }};// NumsDispatcher
import { Dispatcher } from 'flux';
export default new Dispatcher(); 

// NumsStore
class NumsStore extends ReduceStore {
  constructor() {
    super(NumsDispatcher);
  }
  getInitialState() {
    return [0.0.0];
  }
  reduce(state, action) {
    switch (action.type) {
      case NumsActionTypes.INCREASE_COUNT: {
        const nums = [...state]
        nums[action.index] += 1
        return nums;
      }
      case NumsActionTypes.DECREASE_COUNT: {
        const nums = [...state]
        nums[action.index] = nums[action.index] > 0 ? nums[action.index] - 1 : 0
        return nums;
      }
      default:
        returnstate; }}}export default new NumsStore();

// counter
// Note: this is the only way to write./2. Flux /counter
function getStores(. args) {
  return [
    NumsStore,
  ];
}
function getState(preState, props) {
  return {
    ...props,
    nums: NumsStore.getState(),
    increaseCount: NumsActions.increaseCount,
    decreaseCount: NumsActions.decreaseCount,
  };
}
const Counter = (props) = > {
  return <li>
    <button onClick={()= > props.decreaseCount(props.caption)}>-</button>
    <button onClick={()= > props.increaseCount(props.caption)}>+</button>
    {props.caption} Count: {props.nums[props.caption]}
  </li>
}
// need set withProps true, so that can combile props
export default Container.createFunctional(Counter, getStores, getState, { withProps: true })


// total
import * as React from 'react'
import { Container } from 'flux/utils';
import NumsStore from './data/NumsStore';

function getStores() {
  return [ NumsStore ]
}

function getState() {
  return { nums: NumsStore.getState() }
}

const Total = (props) = > {
  return <div>{props.nums.reduce((memo, n) => memo + n, 0)}</div>
}
export default Container.createFunctional(Total, getStores, getState)
Copy the code

Take a look at Flux introduction:

  • A pattern
  • Unidirectional data flow
  • Four parts of a cliche
    • Dispatcher
    • Store
    • Action
    • View

Simply start with words:

  1. Data can only be changed through Action -> Dispatcher -> Store. After the Store data is updated, then emit change event
  2. The View layer listens for data changes and updates the View after receiving the emit event

In fact, it covers the first and second points of the MVC pattern above, while solving the third point

  1. Pubsub, MVC needs their own implementation. And everyone writes differently, it is easy to appear similar to the abovepubsub.emit('all')Such fiddlework is difficult to maintain (so the team also needs a dedicated PubSub, MVC implementation, and specification definition)
  2. More critical: In order to accommodate view updates, both the ControlPanel and counter should be manually monitored for updates in the business layer, and state should be set separately (i.e. Before Flux, Backbone is used to update trigger data, and event listening is performed on componentDidMount, which is similar to the above concept
  3. If you need more data, it’s going to look like this
  1. becauseAction -> Dispatcher -> StoreDefinition, developers no longer need to implement pubsub, MVC, this part of Flux has been defined and implemented, as long as the specification can be written
  2. Flux wraps up the existing components, listening and injecting the required Store data into the components. Components no longer need to listen manually
  3. With the injection of Store data, there is no need to manually write various listening functions and callbacks when relying on multiple data, but only to carry out the corresponding code pattern and inject data

The Flux:

  1. Defines a format/specification that helps you implement data updates without having to manually implement PubSub
  2. It helps you to implement data changes, respond to the operation of the View, do not need to manually handle the listener, and then set the data to the View State processing

Flux’s processing is, arguably, 90% perfect

If you’re interested in how Flux implements these two steps, can you move on to Flux source analysis

but

  1. becauseFluxStoreGroupThat limits everything that’s passed instoredispatcherIt has to be the same, which means that if you want to put differentstoreIntegrate into onecomponentThen you have to use the samedispatcherSo let’s initialize thesestore“Which means, basically, you only need onenew DispatcherCome out
  2. Multiple data stores, there may be dependencies between data, despite the flux designwaitFor, is also very clever, but in the user’s latitude, it is still relatively clever (more hopefully, the data changes all at once)
  3. ContainerWrapping is done in the form of inheriting the original type, and the final data is integrated inthis.stateWhile functional components, data integration is required throughpropsObtain, detailed visible:counter.js – 2.flux
  4. Data changinglogRecording, manualxxStore.addListenerOr comment out this interesting line of code in the Flux source codeFluxContainerSubscriptions console.log
  5. becausegetInitialStateData definition sumreduceData update mode, limitation must be implemented on the Store inheritance class, so only one changereduceAfter the hotreload is performed, the corresponding data state on the original web page that has triggered the change will be returnedinitialState
  6. And two other defects (quote fromRedux — A Cartoon Intro to Redux)
    1. Plug-in architecture: not easy to extend, no suitable place to implement third-party plug-ins
    2. Time travel (retract/redo) functionality:The state object is directly overwritten each time an action is firedBecause Flux defines multiple stores and there is no plugin system, it is difficult to implement time travel

So, we came to the door of Redux

Story writing

See: 3. The story

// actionTypes.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// action.js
import * as actionTypes from './actionTypes'
export const increment = (index) = > {
  return {
    type: actionTypes.INCREMENT,
    index,
  }
}
export const decrement = (index) = > {
  return {
    type: actionTypes.DECREMENT,
    index,
  }
}

// reducer.js
import * as ActionTypes from './actionTypes'
export default (state, action) => {
  const newState = [...state]
  switch (action.type) {
    case ActionTypes.INCREMENT: {
      newState[action.index] += 1
      return newState
    }
    case ActionTypes.DECREMENT:
      newState[action.index] -= 1
      return newState
    default:
      return state
  }
}

// store.js
import { createStore } from 'redux'
import reducer from './reducer'
const initValues = [0.0.0]
export default createStore(reducer, initValues)

// count.js
import * as React from 'react'
import { connect } from 'react-redux'
import * as ActionTypes from './data/actionTypes'
class Counter extends React.Component {
  render() {
    const { decreaseCount, increaseCount, num, caption } = this.props
    return <li>
      <button onClick={()= > decreaseCount(caption, num)}>-</button>
      <button onClick={()= > increaseCount(caption)}>+</button>
      {caption} Count: {num}
    </li>}}const mapStateToProps = (state, props) = > {
  return {
    num: state[props.caption],
  }
}
const mapDispatchToProps = (dispatch, props) = > {
  return {
    decreaseCount(caption, num) {
      if (num > 0) {
        dispatch({
          type: ActionTypes.DECREMENT,
          index: caption,
        })
      }
    },
    increaseCount(caption) {
      dispatch({
        type: ActionTypes.INCREMENT,
          index: caption,
      })
    }
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

// total.js
import * as React from 'react'
import { connect } from 'react-redux'

const Total = (props) = > {
  return <div>{props. Total} {props. Total}</div>
}
const mapStateToProps = (state/* nums */, props) = > {
  return {
    total: state.reduce((memo, n) = > memo + n, 0)}}export default connect(mapStateToProps)(Total)

// controlpanel.js
export default class ControlPanelWrap extends React.Component {
  render() {
    return <Provider store={store}>
      <ControlPanel />
    </Provider>}}Copy the code

At first glance, it feels like there are some differences in the way the code is written. But its internal implementation has actually changed quite a bit.

If you’re interested in how Redux is implemented, can you move on to Redux source analysis

And how the flux defect above is handled

  1. There’s only one dispatch method, on the store
  2. Single data source: one store
  3. ContainerThe function is placed separatelyreact-redux,reduxPart as an accurate/streamlined/subdivided module, only responsible for data update, plug-in system part
  4. throughapplyMiddleWare,enhancercomponseTo implement a complete/complete/elegant plugin/enhancement system, includinglogger,thunk, etc.
  5. willreducePart andstoreThe parts are separated, providing a separatereplaceReducerWhich is used to implement hotReload but will replace the originalstore.getState()The changed data is reinitialized
  6. The other two solutions
    1. Plug-in system, mentioned above
    2. Time travel (retracting/redoing) tool redux-DevTools

other

This article is partially based on the revelation of React state management, and has been streamlined.