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,
- This section is also needed elsewhere (for example, a component of the same level, a sibling of a parent component)
nums
We need to change this as wellnums
What about the data? - 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
- 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 this
nums
When 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:
-
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).
-
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
-
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:
- Data can only be changed through Action -> Dispatcher -> Store. After the Store data is updated, then emit change event
- 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
- Pubsub, MVC needs their own implementation. And everyone writes differently, it is easy to appear similar to the above
pubsub.emit('all')
Such fiddlework is difficult to maintain (so the team also needs a dedicated PubSub, MVC implementation, and specification definition)- 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
- If you need more data, it’s going to look like this
- because
Action -> Dispatcher -> Store
Definition, 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 - Flux wraps up the existing components, listening and injecting the required Store data into the components. Components no longer need to listen manually
- 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:
- Defines a format/specification that helps you implement data updates without having to manually implement PubSub
- 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
- because
FluxStoreGroup
That limits everything that’s passed instore
的dispatcher
It has to be the same, which means that if you want to put differentstore
Integrate into onecomponent
Then you have to use the samedispatcher
So let’s initialize thesestore
“Which means, basically, you only need onenew Dispatcher
Come out- Multiple data stores, there may be dependencies between data, despite the flux design
waitFor
, is also very clever, but in the user’s latitude, it is still relatively clever (more hopefully, the data changes all at once)Container
Wrapping is done in the form of inheriting the original type, and the final data is integrated inthis.state
While functional components, data integration is required throughprops
Obtain, detailed visible:counter.js – 2.flux- Data changing
log
Recording, manualxxStore.addListener
Or comment out this interesting line of code in the Flux source codeFluxContainerSubscriptions console.log- because
getInitialState
Data definition sumreduce
Data update mode, limitation must be implemented on the Store inheritance class, so only one changereduce
After the hotreload is performed, the corresponding data state on the original web page that has triggered the change will be returnedinitialState
- And two other defects (quote fromRedux — A Cartoon Intro to Redux)
- Plug-in architecture: not easy to extend, no suitable place to implement third-party plug-ins
- 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
- There’s only one dispatch method, on the store
- Single data source: one store
Container
The function is placed separatelyreact-redux
,redux
Part as an accurate/streamlined/subdivided module, only responsible for data update, plug-in system part- through
applyMiddleWare
,enhancer
和componse
To implement a complete/complete/elegant plugin/enhancement system, includinglogger
,thunk
, etc. - will
reduce
Part andstore
The parts are separated, providing a separatereplaceReducer
Which is used to implement hotReload but will replace the originalstore.getState()
The changed data is reinitialized - The other two solutions
- Plug-in system, mentioned above
- Time travel (retracting/redoing) tool redux-DevTools
other
This article is partially based on the revelation of React state management, and has been streamlined.