Concept and application of Flux framework

For notes, please refer to Mr. Cheng Mo's "in simple terms, React and Redux"

Flux concept

  • Dispatcher handles action distribution and maintains the dependency between stores
  • Store, which is responsible for storing data and processing data related logic
  • Action, which is provided to the Dispatcher and passes data to the Store
  • View, the view part, 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 receiving this operation, the Action will give it to the Dispatcher, and then the Dispatcher will distribute it to the Store. After receiving the notification, the Store will maintain the relevant data, and then issue a change notification to tell the View layer that the View needs to be updated, then retrieve the data from the Store again, and call the setState method to update the View.

Flux application

Examples

Source branch 02flux




install

npm install --save flux

Dispatcher

src/AppDispatcher.js

//1. Import Dispatcher from flux Library
//Dispatcher is used to dispatch action s
import {Dispatcher} from 'flux'
export default new Dispatcher();

Action

src/AcitonTypes.js

//1. Define the actions type
export const INCREMENT='increment'
export const DECREMENT='decrement'

src/Action.js

//2. Define action constructor
//	This is a function that can generate and distribute action objects (increment and increment)
//	As long as these two functions are called, the corresponding action object will be created and passed through appdispatcher Dispatch send it out
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
    })
}

Store

store object
- Store application status
- accept Dispatcher Action of distribution
- Decide whether to update the application status according to the action
[example]There are three Counter Components, 1 Statistical, 3 Counter Component of the sum of count values
- by Counter Of component services CounterStore
- Total number of services SummaryStore

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. It is used to let other modules in the application read the current count value
    getCounterVlues:function(){
        return conterValues
    },
    
    //2. Define listening
    emitChange:function(){
        this.emit(CHANGE_EVENT)
    },
    addChangeListener:function(callback){
        this.on(CHANGE_EVENT,callback)
    },
    removeChangeListener:function(callback){
        this.removeListener(CHANGE_EVENT,callback)
    }
})

EventEmitter.prototype can turn an object into an EventEmitter object. The instance object of EventEmitter supports the following related functions:

  • The emit function broadcasts a specific event. The first parameter is the event name of string type;
  • on function, add a processing function attached to the specific event of this EventEmitter object. The first parameter is the event name of string type, and the second parameter is the processing function;
  • The removeListener function, contrary to what the on function does, deletes the handler function attached to the specific event of the EventEmitter object.
    The three tasks are completed: updating broadcast, adding listening function and deleting listening function
//3. Register the counter store with the globally unique Dispatcher
//4. Dispatcher has a register function that accepts the callback function as a parameter
//5. The return value is a token, which can be used for synchronization between stores
//	 The return value token will be used later in the SummaryStore and is now saved in counterstore In dispatchtoken
CounterStore.dispatchToken=AppDispatcher.register((action)=>{
    if(action.type===AcitonTypes.INCREMENT){
        counterValues[action.counterCaption]++
        //6. Update listening
        CounterStore.emitChange()
    }else if(action.type===AcitonTypes.DECREMENT){
        counterValues[action.counterCaption]--
        CounterStore.emitChange()
    }
})
export default CounterStore

When a callback function is registered with the Dispatcher through the register function, all action objects sent to the Dispatcher will be passed to the callback function. For example, dispatch an action through Dispatcher:

AppDispatcher.dispatch({
       type:AcitonTypes.INCREMENT,
       counterCaption:'First'
})
//1. Now, the callback function register ed in the counter store will be called
//	 The parameter of register is the action object of this dispatch
//2. The callback function will decide how to update its status according to the action object
//   For example, the meaning of this action: the counter named First needs to add one. There are different operations according to different type s
//3. Therefore, the registered callback function naturally has a pattern, that is, the function body is an if else conditional statement or a switch conditional statement
//	 The jump conditions of conditional statements are all for the type field of the parameter action object
//4. Whether you add one or subtract one, you should call counterstore Emitchange function.
//	 At this time, if there is a caller through CounterStore Addchangelistner focuses on the state changes of CounterStore
//	 This emitChange function call will trigger the execution of the listener function

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);
    }
})

SummaryStore does not store its own state. When getSummary is called, it gets the status calculation directly from the counter store. SummaryStore provides getSummary so that other modules can get the sum of the current values of all 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. SummaryStore is also available through appdispatcher The register function registers the callback function, which is used to accept the distributed action object
//	 In the callback function, only action objects of type INCREMENT and INCREMENT are concerned. And notify the listener through emitChange
//2. waitFor function solves the call order problem
//   At this time, the dispatchToken saved when registering the callback function in the counter store finally comes in handy.
//   When waitFor is called, the Dispatcher is given control
//   Ask the Dispatcher to check whether the callback function represented by dispatchToken has been executed. If it has been executed, connect it directly;
//   If not, call the callback function represented by dispatchToken before waitFor returns

The register function of Dispatcher only provides the function of registering a callback function, but the caller cannot choose to listen only to certain actions when registering. That is, when an action is dispatched, the Dispatcher simply calls all registered callback functions

View

Flux Under the framework, View Not necessarily React, View It is an independent part and can be used in any way UI Library.
Exist in Flux In frame React Components need to realize the following functions:
1.	To read when creating Store Initialize the internal state of the component based on the upper state;
2.	When Store When the upper state changes, the components should immediately update synchronously and keep the internal state consistent;
3.	View If you want to change Store Status, must and can only be distributed action. 

src/views/ControlPanel.js

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

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. The getcountervalues function obtains the current values of all counters
		//Then put this State is initialized to the value of the corresponding caption field
        this.state = {
          count: CounterStore.getCounterValues()[props.caption]
        }    
    }


    //2. Now, the state in the Counter component is a synchronous image of the state on the Flux Store
    //In order to keep the two consistent, when the state on the Counter store changes, the Counter component should also change accordingly
    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 send action when it triggers an event, just put the action object
    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>
        )
    }
}

It can be seen that the Counter component uses the getCounterValues function of CounterStore in two places,
The first is to initialize this in the constructor State
The second is in the onChange function in response to the change of CounterStore state
In order to convert the state of a Store to the state of the React component, 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>
        )
    }
}

summary

  • Introducing Dispatcher objects
  • Define action type
  • Define action constructor
  • Trigger event in view to dispatch action
  • Write store status
    • Define default status values
    • Define variables to get the current state elsewhere
    • Define listening
    • Register a callback and accept the action object sent as a parameter. Then update the store state through the transmitted object; Update the listening (emitChange in the example), and then the caller listens for changes in a store through addChangeListner. If there are changes, the caller calls the callback passed in (onChange in the example) to render the new state in the store to the view

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

The user's operation triggers the distribution of an "action", which will be sent to all Store objects to cause the state change of Store objects, rather than directly causing the state change of components.

What are the benefits of Flux? The most important thing is the management mode of "one-way data flow".

In Flux, if you want to change the interface, you must change the state in the Store. If you want to change the state in the Store, you must send an action object

Lack of Flux

Dependencies between stores

If there is a logical dependency between two stores, you must use the Dispatcher's waitFor function. SummaryStore's handling of action type depends on the fact that CounterStore has already handled it.

SummaryStore identifies CounterStore by the return value dispatchToken of the register function. Of course, the generation of dispatchToken is controlled by CounterStore, that is:

  • CounterStore must make the dispatchToken generated when registering the callback function public
  • SummaryStore must establish a dependency on the dispatchToken of CounterStore in the code

Server side rendering is difficult

In the system of Flux, there is a global Dispatcher, and each Store is a globally unique object, which has no problem for browser applications, but if it is placed on the server, it will have a big problem.

Unlike a web browser that only serves one user, the server must accept the requests of many users at the same time. If each Store is a globally unique object, the status of different requests must be chaotic.

Store is a mixture of logic and state

Store encapsulates data and logic for processing data. However, when we need to dynamically replace the logic of a store, we can only replace the store as a whole, which will not maintain the state stored in the store.

There are also some applications that need to dynamically load different modules according to user attributes in the production environment. Moreover, the dynamic loading module also hopes not to reload the web page. At this time, it also hopes to reload the application logic without modifying the application state. This is hot load

Tags: React

Posted by DMeerholz on Fri, 13 May 2022 13:17:41 +0300