Overview: Extract the state that needs to be shared between components, follow specific conventions, and manage it uniformly, allowing state changes to be predictable.
Store Pattern#
Store the state in an external variable, this.$root.$data
var store = {
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
// Log changes and record points
this.state.message = newValue
},
clearMessageAction () {
this.state.message = ''
}
}
Since the way to change the store's state (mutation) is only through triggering actions, it is easy to track the flow of state changes, and any errors can be clearly identified through logs.
The store does not restrict components to modify the state only through actions
This point evolved into the Flux architecture.
Flux#
Flux is a concept similar to MVC or MVVM, dividing an application into four parts
— View, Action, Dispatcher, Store
The View layer can be implemented using frameworks like Vue or React, and the data in the View comes from the Store. When the Store changes, it emits an event to notify all subscribers (or listeners, depending on the data response technology of different frameworks) of the change.
Flux requires that if the View wants to modify the Store, it must go through a set process:
- The View first tells the Dispatcher to dispatch an action.
- The Dispatcher receives the action emitted by the View and forwards it to the Store.
- The Store triggers the corresponding action to update the data.
- Data updates are accompanied by updates to the View.
Note:
- The role of the Dispatcher is to accept all Actions. Then send them to all Stores (Actions may be triggered by the View or from other places, such as test cases).
- Changes to the Store can only occur through Actions; the Store should not have public Setters, and all Setters should be private, with only public Getters.
- The specific logic for handling Actions is generally placed in the Store.
Flux Characteristics: Unidirectional Flow
Redux#
Similar to the Flux concept,
but modifies some features of Flux:
- An application can have multiple Stores.
- There may be dependencies between multiple Stores.
- The Store also encapsulates the logic for handling data.
Store#
In Redux, there is only one Store, and all application data resides in this single Store. The State of the Store cannot be modified directly; it can only return a new State each time. Redux provides a createStore function to generate the Store.
import { createStore } from 'redux';
const store = createStore(fn);
The Store allows the use of the store.subscribe method to set listener functions, which will automatically execute whenever the State changes. This way, regardless of how the View is implemented, as long as the View's update function is subscribed, it will automatically render after the State changes. For example, in React, you can subscribe the component's render method or setState method.
Action#
Like Flux, Redux also has Actions, which are notifications sent by the View to inform the Store that the State needs to change. Actions must have a type attribute representing the name of the Action, and other attributes can be set as parameters for reference during State changes.
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
Redux can use Action Creators to generate multiple Actions in bulk.
Reducer#
Redux does not have the concept of a Dispatcher; the Store already integrates the dispatch method. store.dispatch() is the only method for the View to emit Actions.
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
Redux uses a pure function called Reducer to handle events. After the Store receives an Action, it must provide a new State (as mentioned earlier, the State of the Store cannot be modified directly; it can only return a new State), so that the View will change. This process of calculating the State is called Reduce.
A pure function has no side effects:
- For the same input, it will always produce the same output.
- It does not affect external variables and is not affected by them.
- It cannot modify its parameters.
Redux derives the new state based on the application's state and the current action: (previousState, action) => newState
In comparison to Flux: (state, action) => state
question: Why is it called a Reducer?
—— Reduce is a concept from functional programming, often mentioned alongside map. Simply put, map is for mapping, while reduce is for summarizing.
Mapping transforms a list into another list based on certain rules, while reduce merges a list through specific rules, which can also be understood as performing a series of operations on an initial value to return a new value.
Overall Process#
- The user emits an Action through the View:
store.dispatch(action);
- The Store automatically calls the Reducer, passing in two parameters: the current State and the received Action. The Reducer will return a new State.
let nextState = xxxReducer(previousState, action);
- Once the State changes, the Store will call the listener function.
store.subscribe(listener);
- The listener can obtain the current state through store.getState(). If using React, this can trigger a re-render of the View.
function listener() {
let newState = store.getState();
component.setState(newState);
}
Comparison with Flux#
Comparing with Flux: In Flux, each Store operates independently, with each Store only responsible for its corresponding View, notifying only the relevant View during updates:
In Redux, all sub-Reducers are managed uniformly by the root Reducer, and changes in each sub-Reducer must go through the integration of the root Reducer:
In simple terms, Redux has three main principles: _Single Source of Truth: Flux's data source can be multiple. _State is Read-Only: Flux's State can be modified freely. * Use Pure Functions to Make Changes: Flux does not necessarily use pure functions for modifications.
Redux, like Flux, is also a unidirectional data flow.
Relationship with React#
Redux is similar to Flux, being a concept or specification that is not tied to React. Redux supports React, Angular, Ember, jQuery, and even plain JavaScript.
However, because React incorporates functional programming concepts and also follows unidirectional data flow, it pairs well with Redux, so Redux is generally used for state management. To simplify the binding of Redux and React UI, a library called react-redux is typically used in conjunction, which is officially released by React (if react-redux is not used, manually handling the binding of Redux and UI requires writing a lot of repetitive code, which is prone to errors, and many UI rendering logic optimizations may not be handled well).
Redux divides React components into container components and presentational components. Container components are generally generated through the connect function, subscribing to global state changes, and through the mapStateToProps function, they can filter global state. Presentational components do not directly obtain data from the global state; their data comes from parent components.
If a component needs both UI presentation and business logic processing, it should be split into a container component wrapping a presentational component.
Redux-saga#
Redux handles asynchronous operations, resulting from adding middleware.
Official documentation: Redux-Saga
Dva#
Official definition: Dva is primarily a data flow solution based on redux and redux-saga, and to simplify the development experience, Dva also integrates react-router and fetch, so it can also be understood as a lightweight application framework.
In simple terms, it organizes code written using react-redux and redux-saga more reasonably and makes maintenance easier.
Previously, when we discussed concepts like redux, react-redux, and redux-saga, everyone must have felt overwhelmed by terms like action, reducer, saga, etc., having to switch between these JS files to write a feature. What Dva does is simple: it allows these things to be written together without needing to separate them.
For example:
// Previously, the way to write was to create sagas/products.js, reducers/products.js, and actions/products.js, and separate saga, action, and reducer writing, switching back and forth.
app.model({
// namespace - corresponds to the key value of the reducer when combined into the rootReducer
namespace: 'products',
// state - corresponds to the initialState of the reducer
state: {
list: [],
loading: false,
},
// subscription - executes after the DOM is ready
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
// effects - corresponds to saga and simplifies usage
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
// reducers - traditional reducers
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});
MobX#
Official website: MobX Chinese Documentation
In contrast to the unidirectional data flow solution of the Flux system, MobX's philosophy is: Anything derived from application state should automatically update — as soon as the state changes, everything that uses that state will automatically change.
The philosophy of Flux or Redux mainly revolves around functional programming (FP), which can feel a bit heavy to learn. In contrast, MobX is closer to object-oriented programming, wrapping the state into observable objects that drive various changes. What is observable? It means MobX is watching the state. As soon as the state changes, everything that uses it will change accordingly. This way, the entire View can be driven by the state.
const obj = observable({
a: 1,
b: 2
})
autoRun(() => {
console.log(obj.a)
})
obj.b = 3 // Nothing happens
obj.a = 2 // The observe function's callback is triggered, console outputs: 2
In the example above, the obj.a property is being used, so as soon as obj.a changes, all places that use it will be called. autoRun is the observer that watches all dependencies on obj.a, collecting all dependencies on obj.a. When obj.a changes, the observer will trigger all dependencies to update.
MobX, like Flux and Redux, is independent of specific front-end frameworks, meaning it can be used with React (mobx-react) or Vue (mobx-vue). Generally, it is more commonly used with React and rarely with Vue, as Vuex itself is similar to MobX and very flexible. If we use MobX with React or Vue, we can see that many setState() and this.state.xxx handling can be eliminated.