For notes, refer to “React and Redux” by Cheng Mo.

Flux concept

  • Dispatcher handles action distribution and maintains dependencies between stores
  • Store, which is responsible for storing data and handling data-related logic
  • Action, which feeds the Dispatcher, passes data to the Store
  • View, the View section, is responsible for displaying the user interface

First, an event will be generated, which is generally an operation performed by the user on the interface. After the Action gets the operation, it will be handed to the Dispatcher, which will then distribute it to the Store. The Store will maintain the relevant data after receiving the notification, and then send a change notice. Tell the View layer that it needs to update the View, retrieve the data from the Store, and call the setState method to update the View.

Flux application

The sample

Sample source code in branch 02flux

The installation

npm install --save flux
Copy the code

Dispatcher

src/AppDispatcher.js

//1. Import Dispatcher from the Flux repository
// The Dispatcher is used to dispatch actions
import {Dispatcher} from 'flux'
export default new Dispatcher();
Copy the code

Action

src/AcitonTypes.js

//1. Define actions types
export const INCREMENT='increment'
export const DECREMENT='decrement'
Copy the code

src/Actions.js

//2. Define the action constructor
// This is a function that generates and distributes action objects (increment and Decrement)
// Whenever these two functions are called, corresponding action objects are created and dispatched via AppDispatcher.dispatch
import * as AcitonTypes from './ActionTypes';
import AppDispatcher from './AppDispatcher'

export const increment = (counterCaption) = >{
    AppDispatcher.dispatch({
        type:AcitonTypes.INCREMENT,
        counterCaption:counterCaption
    })
}

export const decrement = (counterCaption) = >{
    AppDispatcher.dispatch({
        type:AcitonTypes.DECREMENT,
        counterCaption:counterCaption
    })
}
Copy the code

Store

Store object - Stores application status - accepts actions from the Dispatcher - determines whether to update application status based on the actionCopy the code
[Example] There are three Counter components. One component that counts the sum of the three Counter counts - CounterStore for Counter components - SummaryStore for totalsCopy the code

src/stores/CounterStore.js

import AppDispatcher from '.. /AppDispatcher'
import * as AcitonTypes from '.. /ActionTypes';
import {EventEmitter} from 'events';
const CHANGE_EVENT='changed'
const counterValues = {
    'First': 0.'Second': 10.'Third': 30
};
const CounterStore=Object.assign({},EventEmitter.prototype,{
    //1. Used to allow other modules in the application to read the current count
    getCounterVlues:function(){
        return conterValues
    },
    
    //2. Define listener
    emitChange:function(){
        this.emit(CHANGE_EVENT)
    },
    addChangeListener:function(callback){
        this.on(CHANGE_EVENT,callback)
    },
    removeChangeListener:function(callback){
        this.removeListener(CHANGE_EVENT,callback)
    }
})
Copy the code

EventEmitter. Prototype Allows an object to become an EventEmitter object. EventEmitter instance objects support the following functions:

  • Emit function that broadcasts a specific event, taking the event name as a string as the first argument;
  • On, which adds a handler attached to the EventEmitter object. The first parameter is the event name as a string, and the second parameter is the handler.
  • The removeListener function, as opposed to the ON function, removes handlers attached to specific events in the EventEmitter object.

Three complete: update broadcast, add listener and delete listener

//3. Register CounterStore with a globally unique Dispatcher
// Dispatcher has a register function that accepts a callback function as an argument
5. The return value is a token that can be used for synchronization between stores
/ / the return value token used in a later SummaryStore, now kept in CounterStore. DispatchToken
CounterStore.dispatchToken=AppDispatcher.register((action) = >{
    if(action.type===AcitonTypes.INCREMENT){
        counterValues[action.counterCaption]++
        //6. Update listener
        CounterStore.emitChange()
    }else if(action.type===AcitonTypes.DECREMENT){
        counterValues[action.counterCaption]--
        CounterStore.emitChange()
    }
})
export default CounterStore
Copy the code

When a callback is registered with the Dispatcher through the Register function, all action objects sent to the Dispatcher are passed to the callback function. For example, send an action via Dispatcher:

AppDispatcher.dispatch({
       type:AcitonTypes.INCREMENT,
       counterCaption:'First'
})
//1. The callback function is now called when you register with CounterStore
// Register is the action object that is dispatched
//2. The callback function can update its state based on the action object
// A counter named First is incremented. Depending on the type, there are different operations
//3. So there is a natural pattern for registered callback functions. The body of the function is if-else or switch condition
// Jump conditions for conditional statements are for the type field of the action object
//4. Either increment or subtraction ends with a call to counterStore. emitChange.
/ / at this point, if you have the caller by CounterStore. AddChangeListner attention CounterStore state changes
// The emitChange call causes the listener to execute
Copy the code

src/stores/SummaryStore.js

import CounterStore from './CounterStore'
import AppDispatcher from '.. /AppDispatcher'
import * as AcitonTypes from '.. /ActionTypes';
import {EventEmitter} from 'events';
const CHANGE_EVENT='changed'

function computeSummary(counterValues){
    let summary=0
    for(const key in counterValues){
        if(counterValues.hasOwnProperty(key)){
            summary+=counterValues[key]
        }
    }
    return summary
}

const SummaryStore=Object.assign({},EventEmitter.prototype,{
    getSummary:function(){
        return computeSummary(CounterStore.getCounterValues())
    },

    emitChange: function() {
    	this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
    	this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
    	this.removeListener(CHANGE_EVENT, callback); }})Copy the code

The SummaryStore does not store its own state. When getSummary is called, it gets the state calculation directly from the CounterStore. The SummaryStore provides getSummary so that other modules can get the sum of the current values of all the counters.

SummaryStore.dispatchToken=AppDispatcher.register((action) = >{
    if((action.type===AcitonTypes.INCREMENT)||(action.type===AcitonTypes.DECREMENT)){
        AppDispatcher.waitFor([CounterStore.dispatchToken]);
        SummaryStore.emitChange()
    }
})
export default SummaryStore

//1. The SummaryStore also uses the AppDispatcher.register function to register a callback that accepts a distributed action object
// In callback functions, only INCREMENT and DECREMENT action objects are focused. And notify listeners via emitChange
//2. The waitFor function resolves the call order problem
// At this point, the dispatchToken saved when registering the callback function in CounterStore finally comes into play.
// When calling waitFor, give control to the Dispatcher
// Have Dispatcher check whether the callback function represented by dispatchToken has been executed.
// If not, waitFor returns after invoking the callback represented by dispatchToken
Copy the code

The Dispatcher register function only provides the ability to register a callback function, but does not allow the caller to choose to listen only for certain actions while registering. When an action is dispatched, the Dispatcher simply calls all registered callback functions

View

Under the Flux framework, a View does not have to use React. The View itself is an independent part and can be implemented using any UI library. The React component in the Flux framework needs to implement the following functions: 1. Initialize the internal state of the component by reading the state on the Store. 2. When the Store state changes, components should immediately update the internal state to keep consistent; 3. If the View wants to change the state of the Store, it must and can only issue actions.Copy the code

src/views/ControlPanel.js

class ControlPanel extends React.Component{
    render(){
        return(
        	<div>
            	<Counter caption="First" />
                <Counter caption="Second" />
                <Counter caption="Third" />
                <hr/>
                <Summary/>
            </div>)}}Copy the code

src/views/Counter.js

import * as Actions from '.. /Actions'
import CounterStore from '.. /stores/CounterStore'

class Counter extends React.Component{
    constructor(props) {
        super(props);
        this.onChange = this.onChange.bind(this);
        this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
        this.onClickDecrementButton = this.onClickDecrementButton.bind(this);
        / / 1. CounterStore. GetCounterValues function get all the current value of the counter
		// Then initialize this.state to the value of the caption field
        this.state = {
          count: CounterStore.getCounterValues()[props.caption]
        }    
    }


    //2. State in the Counter component is now a synchronous mirror of the state on the Flux Store
    // To keep them consistent, when the state changes on CounterStore, the Counter component also changes
    componentDidMount(){
        CounterStore.addChangeListener(this.onChange)
    }
    componentWillUnmount(){
        CounterStore.removeChangeListener(this.onChange)
    }    
    onChange(){
        const newCount =CounterStore.getCounterValues()[this.props.caption]
        this.setState({count: newCount});
    }
    
    //3. Update the status only when the status is inconsistent
    shouldComponentUpdate(nextProps, nextState) {
        return(nextProps.caption ! = =this.props.caption) || (nextState.count ! = =this.state.count);
    }
    
	//4. Let the React component issue an action when it triggers an event
    onClickIncrementButton(){
        Actions.increment(this.props.caption)
    }
    onClickDecrementButton(){
        Actions.decrement(this.props.caption)
    }
    
    render(){
        const {caption} =this.props
        return(
        	<div>
            	<input type="button" onClick={this.onClickIncrementButton} value="+"/>
                <input type="button" onClick={this.onClickDecrementButton} value="-"/>
                <span>{caption} count:{this.state.count}</span>
            </div>)}}Copy the code

As you can see, the CounterStore getCounterValues function is used in two places in the Counter component, The first is when this. State is initialized in the constructor and the second is when the same Store state is initialized in the onChange function in response to the CounterStore state change. To convert to the React component state, there are two repeated calls

src/views/Summary.js

import SummaryStore from '.. /stores/SummaryStore'
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.onUpdate = this.onUpdate.bind(this);
        this.state={
            sum:SummaryStore.getSummary()
        }
    }
    componentDidMount(){
        SummaryStore.addChangeListener(this.onUpdate)
    }
    componentWillUnmount(){
        SummaryStore.removeChangeListener(this.onUpdate)
    }    
    onUpdate() {
        this.setState({
          sum: SummaryStore.getSummary()
        })
    }
    render(){
        return(
        	<div>
            	total:{this.state.sum}
            </div>)}}Copy the code

conclusion

  • Import the Dispatcher object
  • Defining action types
  • Define the Action constructor
  • View triggers an event to dispatch an action
  • Writing store state
    • Define default status values
    • Define variables to let someone else get the current state
    • Define listener
    • Register a callback that accepts the action object as an argument. Then update the store state with the passed object; The update listener (emitChange in the example) then listens for changes in a store through addChangeListner and calls the incoming callback (onChange in the example) to render the new state in the store into the view

Under the Flux architecture, the application state is placed in the Store. The React component only acts as a View and passively renders according to the Store state. In the example above, the React component still has its own state, but it’s completely reduced to a mapping of the Store component rather than actively changing data.

User actions trigger dispatch of an “action” that is sent to all Store objects, causing state changes in Store objects rather than directly causing state changes in components.

What benefits does Flux bring? The most important is the management of “one-way data flow”.

In Flux, to change the interface, the state in Store must be changed, and to change the state in Store, an Action object must be dispatched

The shortage of the Flux

Dependencies between stores

If there is a logical dependency between two stores, the Dispatcher waitFor function must be used. The processing of the SummaryStore action type depends on what the CounterStore has already done.

The SummaryStore is identified by the register value of dispatchToken, which is generated by a CounterStore.

  • CounterStore must expose the dispatchToken generated when the callback is registered
  • SummaryStore must rely on CounterStore’s dispatchToken in its code

Server-side rendering is difficult

In Flux, there is a global Dispatcher, and each Store is a globally unique object. This is fine for browser-side applications, but it can be a big problem on the server side.

Instead of a browser web page serving only one user, the server receives requests from many users at the same time, and if each Store is a globally unique object, the state of the different requests must be confused.

Store is a mixture of logic and state

The Store encapsulates the data and the logic that processes it. However, when we need to dynamically replace the logic of a Store, we can only replace the Store as a whole, which cannot preserve the state of the Store.

Some applications need to dynamically Load different modules based on user attributes in the production environment. In addition, dynamic loading modules also want to be able to reload the application logic without changing the application state. This is called Hot Load.