1. Foreword
When talking about React, we have to mention redux. Although not every project must use Redux, as a new generation of migrant workers (national certification), we must understand the relevant concepts so that we can start immediately. In recent projects, we didn't use Redux very much. In order to prevent forgetting, we thought about writing down some learning experiences to review the old and know the new.
2. Traditional MVC framework
Look at this picture first
The full name of MVC is Model View Controller, which is the abbreviation of Model View Controller. It is a model of software design.
V is the View view, which refers to the interface that users see and interact with.
M, that is, Model, is management data, and many business logic is completed in the Model. Among the three components of MVC, the Model has the most processing tasks.
C is the Controller, which means that the Controller accepts the user's input and calls the model and view to complete the user's requirements. The Controller itself does not output anything and do any processing. It just receives the request and decides which model component to call to process the request, and then determines which view to use to display the returned data.
It seems that the data flow of MVC framework is ideal. The request comes to the Controller first, and the Controller calls the data in the Model to the View for rendering. However, in the actual project, the Model and View are allowed to communicate directly. Then the result appears, as shown in the following figure:
3,Flux
React is just a V (view layer) in MVC. It only cares about the rendering in the page. Once there is data management, the ability of react itself is not enough to support projects with complex component structure. In traditional MVC, Model and Controller are needed.
Facebook was not satisfied with the MVC framework in the world at that time, so it had Flux, but Flux was not an MVC framework. It was a new idea.
In 2013, Facebook unveiled react and launched the Flux framework at the same time. React was actually intended to replace jQuery, and Flux can actually be used to replace backbone js,Ember.js and a series of front-end JS frameworks of MVC architecture.
In fact, the application of Flux in React is similar to the role of Vuex in Vue, but in Vue, Vue is a complete mvvm framework, and Vuex is only a global plug-in.
- View: view layer
- ActionCreator: message sent by the view layer (such as mouseClick)
- Dispatcher: used to receive Actions and execute callback functions
- Store (data layer): it is used to store the application status. Once it changes, it will remind Views to update the page
Flux process:
-
The component obtains the data saved in the store and mounts it in its own state
-
The user generates an action and calls the actions method
-
actions receives the user's operation and performs a series of logical code and asynchronous operations
-
Then, actions will create the corresponding action with an identifying attribute
-
actions calls the dispatcher's dispatch method to pass action s to the dispatcher
-
After receiving the action and judging by the identification information, dispatcher calls the store to change the data.
-
After the method of store is called, it changes the state and triggers one of its own events
-
After the state of the store is changed, the event is triggered, and the handler of the event will notify the view to obtain the latest data
4,Redux
4.1 introduction to redux
React is just an abstraction layer of DOM, not a complete solution for Web applications. There are two aspects that it does not involve.
- Code structure
- Communication between components
In 2013, Facebook proposed the idea of Flux architecture, which triggered many implementations.
In 2015, Redux appeared, integrating Flux with Functional programming Together, it has become the most popular front-end architecture in a short time.
If you don't know if you need Redux, you don't need it
Only when you encounter problems that React can't solve, you need Redux
In short, if your UI layer is very simple and there is not much interaction, Redux is unnecessary. It will increase the complexity if it is used.
Projects that do not need to use Redux:
- The user's usage is very simple
- There is no collaboration between users
- There is no need to interact with the server and WebSocket is not used
- The View layer only gets data from a single source
Projects requiring Redux:
- The user's usage is complex
- Users with different identities have different usage methods (such as ordinary users and administrators)
- Multiple users can collaborate
- Interact with the server a lot, or use WebSocket
- View wants to get data from multiple sources
From the component level, what kind of Redux is needed:
- The status of a component needs to be shared
- A state needs to be available anywhere
- A component needs to change the global state
- One component needs to change the state of another component
4.2 design idea of redux:
- Web application is a state machine, and the view and state correspond to each other one by one.
- All States are saved in an object (unique data source).
be careful: flux,redux You don't have to react Used together because flux and redux Is the complete architecture in learning react When I was young, I just react Components as redux The view layer in is used.
4.3 three principles for the use of redux:
-
Single source of truth
-
State is read-only
-
Changes are made with pure function
4.4 implementation of redux by native simulation
reducer.js file
Pure function: 1: The same input parameters get the same output 2: The input parameter cannot be modified
const reducer = ( state , action ) => { action = action ||{ type : ''} switch(action.type){ case 'increment': return { ...state, count : state.count + 1 } case " decrement": return { ...state, count : state.count - 1 } default: return state } } export { reducer }
store.js file
import { reducer } from './reducer' let state = { count: 0 } const createStore = () => { // getState get state const getState = () => state // Observer mode const listeners = [] // subscribe subscription const subscribe = listener => listeners.push(listener) const dispatch = action => { // console.log(reducer(state, action)) state = reducer(state, action) // Publish publish listeners.forEach(listener => listener()) } return { dispatch, getState, subscribe } } const store = createStore() const render = () => { document.querySelector('#count').innerHTML = store.getState().count } store.subscribe(render) export default store
App.js any component
import React, { Component } from 'react' import store from './Store' class App extends Component { componentDidMount(){ store.dispatch() } render() { return ( <div > <button onClick={store.dispatch.bind(this,{type : 'increment'})}>+</button> <span id='count'></span> <button onClick={store.dispatch.bind(this,{type : 'decrement'})}>-</button> </div> ) } } export default App
4.4. 1. Overall thinking
(1) The createStore () method is defined in the store file. The createStore method includes three methods:
- getState: change the state through reducer and get the latest state
- Subscribe: publish subscribe mode. Subscribe through subscribe, that is, use an empty array to push the method
- dispatch: call action, which contains two operations: one is to call the pure function of reducer (state, action) to obtain the latest state; the other is publish, which uses the method array subscribed through the empty array subscribe just now to perform forEach traversal, that is, to complete the so-called distribution.
Therefore, every time you dispatch, the status will be changed; In addition, publish will publish the things just subscribed to. So the last thing createStore return s is dispatch, getState and subscribe.
Outside the createStore, you can define the render function and subscribe for the first time, and introduce reducer at the beginning of the store file.
(2) In the reducer (state, action) pure function, the characteristics of the pure function are as follows: 1. The same input parameters get the same output. 2. The output result of the function cannot be deconstructed and modified. It can be distinguished by action.type, and then perform different operations to return a new state.
(3) Introduce a store file into a component, call store.dispatch, and pass in different type s for different operations.
4.5 details redux
Install redux
yarn add redux
4.5. 1. What's in redux
After installing Redux, you can create a js file at will, import the Redux just installed by require, and print it. You can see what is included in redux
It can be seen that redux contains:
- _ DO_NOT_USE__ActionTypes object
- applyMiddleware
- bindActionCreators
- combineReducers
- compose
- createStore
These methods (described in detail below)
4.5. 2. What's in the store
We deconstruct the createStore from redux and pass the written reducer into it as a parameter for printing and viewing
reducer.js file
const defaultState = {count: 0} const reducer = (state = defaultState, action) => { switch(action.type) { case 'increment': return { ...state, count: state.count + 1 } case 'decrement': return { ...state, count: state.count - 1 } default: return state } } module.exports = reducer
It can be seen that, like the redux we used in our own native simulation, this store contains:
- dispatch
- subscribe
- getState
- replaceReducer
- observable
Other methods
store.dispatch()
It's still the reducer file just now. This time we store Dispatch () dispatches a type and prints the store Getstate(), see what's inside
Call several times and change several times
store.dispatch() and store subscribe()
subscribe is equivalent to subscription, and each dispatch is equivalent to publishing, which is different from the object in vue defineproperty()
4.6. Implement TodoList with react+redux
Install react redux: provide Provider and connect (note the difference between react redux and react redux)
yarn add react-redux
React Redux contains:
- mapStateToProps: map state to props of the current component
- mapDispatchToProps: map a method to the props of the current component, which dispatches events through dispatch.
mapStateToProps and mapDispatchToProps return an object through return to get data and functions in the object
(attach a flowchart of redux)
4.6. 1. Basic version (without action creator and Middleware)
External entrance index JS file
Deconstruct the Provider from react Redux, then reference the store and inject the store into the Provider, so that the state and various methods written on props can be obtained through connect (mapStateToProps, mapDispatchToProps)
import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import App from './13-redux/04-todolist/TodoList' import store from './13-redux/04-todolist/store/index' ReactDOM.render( <Provider store={store}> <App ></App> </Provider>, document.querySelector('#root') )
TodoList file:
import React, { Component } from 'react' import Form from './Form' import List from './List' //Introduce two separate components class App extends Component { render() { return ( <div> <Form></Form> <List></List> </div> ) } } export default App
List file
import React, { Component } from 'react' import { connect } from 'react-redux' //Map state to props of current component const mapStateToProps = ( state) => { return{ list : state.list } } //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { deleteData: (index) => { dispatch({ type : 'DELETE_DATA', index }) } } } @connect(mapStateToProps,mapDispatchToProps) class List extends Component { componentDidMount(){ console.log('this' ,this) } handleClick = (index) => { return()=>{ this.props.deleteData(index) } } render() { return ( <ul> { this.props.list.map((value ,index)=>{ return( <li key={index}> {value} <button onClick={this.handleClick(index)}>delete</button> </li> ) }) } </ul> ) } } export default List
Form file
import React, { Component } from 'react' import { connect } from 'react-redux' //Map state to props of current component const mapStateToProps = () => { return{} } //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { putData: (task) => { dispatch({ type : 'PUT_DATA', task }) } } } @connect(null,mapDispatchToProps) class Form extends Component { state = { task : '' } handleChange = (e) =>{ this.setState({ task : e.target.value }) } handleKeyUp = (e) => { if(e.keyCode === 13){ // console.log(this) this.props.putData(this.state.task) this.setState({ task : '' }) } } handleClick = () => { this.props.putData(this.state.task) this.setState({ task : '' }) } render() { return ( <div> <input type="text" name="" id="" value={this.state.task} onChange={this.handleChange} onKeyUp={this.handleKeyUp} /> <button onClick={this.handleClick}>determine</button> </div> ) } } export default Form
index under store
The main purpose here is to introduce reducer and throw out store
//Deconstruct createStore from react Redux import { createStore } from 'redux' import reducer from './reducer' //Introduce reducer into createStore const store = createStore(reducer) export default store
reducer under store
Reducer is a pure function, so defaultState is an object, no matter which type type is in reducer, it should be object type returned, so as to ensure the same incoming and outgoing.
const defaultState = { arr:[ 'Apple','Banana','Grape' ], list : [ 'Task 1' , 'Task 2' ] } const reducer = ( state = defaultState,action) => { switch(action.type){ case 'LOAD_DATA': return state; case 'PUT_DATA': return { //... state is to prevent defaultState from having other attributes besides the list array ...state, list : [ ...state.list, action.task ] } case 'DELETE_DATA': let list1 = state.list.filter((v,i)=>{ return i!==action.index }) return { //... state is to prevent defaultState from having other attributes besides the list array ...state, list : list1 } default : return state } } export default reducer
design sketch
Implementation principle
- At the beginning, this is printed in the list file. It is found that deleteData and list have been attached to props. The credit belongs to the Provider and connect deconstructed from react redux
- Enter a value in the input box in the Form file and click OK to trigger the putData function attached to props and send a dispatch with type put_ For data type functions, the task parameter is the input content, and then the reducer in the store identifies the action Type, action The task is stored in the list, and the addition is completed
- Identify the list data attached to props in the list file for rendering; Click Delete to trigger the deleteData function attached to props and send a dispatch with type delete_ For a function of data type, index is a key value of the rendered list, and then the reducer in the store identifies the action Type. Index in the list array is not equal to action The index is filtered out and re assigned to the list, which completes the deletion
4.6. 2. Basic version (with action creator and without Middleware)
Literally, Action Creators are files that integrate all Action actions and help us extract various dispatch dispatch functions.
External entrance index JS file
Same as 4.6 Part of 1
TodoList file:
Same as 4.6 Part of 1
List file
import React, { Component } from 'react' import { connect } from 'react-redux' import { deleteAction } from './store/actionCreator' //Map state to props of current component const mapStateToProps = ( state) => { return{ list : state.list } } //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { deleteData: (index) => { // After introducing actionCreator dispatch(deleteAction(index)) } } } @connect(mapStateToProps,mapDispatchToProps) class List extends Component { componentDidMount(){ // console.log('this' ,this) } handleClick = (index) => { return()=>{ this.props.deleteData(index) } } render() { return ( <ul> { this.props.list.map((value ,index)=>{ return( <li key={index}> {value} <button onClick={this.handleClick(index)}>delete</button> </li> ) }) } </ul> ) } } export default List
Form file
import React, { Component } from 'react' import { connect } from 'react-redux' import { putdataAction } from './store/actionCreator' //Map state to props of current component const mapStateToProps = () => { return{} } //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { putData: (task) => { //After introducing actionCreator dispatch(putdataAction(task)) } } } @connect(null,mapDispatchToProps) class Form extends Component { state = { task : '' } handleChange = (e) =>{ this.setState({ task : e.target.value }) } handleKeyUp = (e) => { if(e.keyCode === 13){ // console.log(this) this.props.putData(this.state.task) this.setState({ task : '' }) } } handleClick = () => { this.props.putData(this.state.task) this.setState({ task : '' }) } render() { return ( <div> <input type="text" name="" id="" value={this.state.task} onChange={this.handleChange} onKeyUp={this.handleKeyUp} /> <button onClick={this.handleClick}>determine</button> </div> ) } } export default Form
index under store
Same as 4.6 Part of 1
reducer under store
Same as 4.6 Part of 1
actionCreator under store
aciton is integrated into one file and dispatch is distributed uniformly to facilitate management.
const deleteAction = (index) => { return { type : 'DELETE_DATA', index } } const putdataAction = (task) => { return { type : 'PUT_DATA', task } } export { deleteAction, putdataAction }
design sketch
4.6. 3. Upgraded version (with Middleware version)
4.6. 3.1. What is Middleware
In 4.6 1, we implemented a simple version of react+redux, but in the actual development work, we often have some asynchronous operations, such as ajax requests. At this time, the above basic writing method can not meet our business requirements, so we need to introduce some tools that can meet asynchronous operations, namely Middleware (which can meet the requirements of asynchronous operations).
The following figure is a flowchart of react+redux:
At the same time, the place marked by the red dot is where we perform asynchronous operations. as a result of:
- store is equivalent to an aggregation container and does not do detailed code operations
- reducer is equivalent to a pure function and cannot perform operations with side effects
- The React Components component can perform asynchronous operations and synchronous distribution, but this shows that redux depends on React, which is contrary to the principle that redux is independent of React
- Action Creator simply distributes an object, which is not suitable for
Therefore, we can only perform asynchronous operations after action creators.
4.6. 3.2. Do not use Middleware
Assuming there is no reference to middleware, we change the flat object into multiple sets of one-layer functions outside in actionCreator, which means that the dispatch in the component is not a flat object, but an asynchronous function, which contains flat objects, as shown in the figure:
Add test code in the List file:
Leave the other files unchanged and start running
It is found that the console reports an error, prompting us that if the asynchronous operation is carried out directly after the Action Creator, it cannot succeed, because the Actions must be a flat object in the end, so we must use the Middleware to help us.
4.6. 3.3. Using Middleware
Common middleware include Redux thunk, Redux saga, etc. Here we introduce Redux thunk middleware
Install Redux thunk
yarn add redux-thunk
index under store
Deconstruct applymeddleware from redux, introduce thunk, and pass in createStore
import { createStore ,applyMiddleware} from 'redux' import reducer from './reducer' import thunk from 'redux-thunk' //Once the middleware is hung, the dispatch in the component is bound to be stopped by the middleware const middleware = applyMiddleware(thunk) const store = createStore( reducer , middleware) export default store
Other files remain unchanged. Run the program again and the page can be displayed normally
So we can see that Middleware helps us run asynchronous operations.
4.6. 3.4. Asynchronous ajax request using Middleware
Scenario: suppose that the data requested from ajax is displayed at the beginning of the page (we use fetch simulation), and it can be added and deleted manually.
External entrance index JS file
Deconstruct the Provider from react Redux, then reference the store and inject the store into the Provider, so that the state and various methods written on props can be obtained through connect (mapStateToProps, mapDispatchToProps)
mport React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import App from './13-redux/05-todolist2/App' import store from './13-redux/05-todolist2/store/index' ReactDOM.render( <Provider store={store}> <App ></App> </Provider>, document.querySelector('#root') )
TodoList file:
import React, { Component } from 'react' import Form from './Form' import List from './List' //Introduce two separate components class App extends Component { render() { return ( <div> <Form></Form> <List></List> </div> ) } } export default App
List file
import React, { Component } from 'react' import { connect } from 'react-redux' import { deleteAction , setdataAction } from './store/actionCreator' //Map state to props of current component const mapStateToProps = ( state) => { return{ list : state.list } } //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { deleteData: (index) => { //Before introducing actionCreator // dispatch({ // type : 'DELETE_DATA', // index // }) // After introducing actionCreator dispatch(deleteAction(index)) }, setData : ()=>{ dispatch(setdataAction()) }, } } @connect(mapStateToProps,mapDispatchToProps) class List extends Component { componentDidMount(){ //The scenario stipulates that the list data is displayed at the beginning this.props.setData() } handleClick = (index) => { return()=>{ this.props.deleteData(index) } } render() { return ( <ul> { this.props.list?.map((value ,index)=>{ return( <li key={index}> {value.positionName} <button onClick={this.handleClick(index)}>delete</button> </li> ) }) } </ul> ) } } export default List
Form file
import React, { Component } from 'react' import { connect } from 'react-redux' import { putdataAction } from './store/actionCreator' //Map state to props of current component // const mapStateToProps = () => {} //Map the dispatch to the props of the current component const mapDispatchToProps = (dispatch) => { return { putData: (task) => { // Before introducing actionCreator // dispatch({ // type : 'PUT_DATA', // task // }) //After introducing actionCreator dispatch(putdataAction(task)) } } } @connect(null,mapDispatchToProps) class Form extends Component { state = { task : '' } handleChange = (e) =>{ this.setState({ task : e.target.value }) } handleKeyUp = (e) => { if(e.keyCode === 13){ this.props.putData(this.state.task) this.setState({ task : '' }) } } render() { return ( <div> <input type="text" name="" id="" value={this.state.task} onChange={this.handleChange} onKeyUp={this.handleKeyUp} /> </div> ) } } export default Form
index file under store
import { createStore ,applyMiddleware} from 'redux' import reducer from './reducer' import thunk from 'redux-thunk' //Once the middleware is hung, the dispatch in the component is bound to be stopped by the middleware const middleware = applyMiddleware(thunk) const store = createStore(reducer , middleware) export default store
actionCreator file under store
The setdataAction method invoked in the List component is asynchronous operation to distribute non flat data. Here is the arrowhead function that middleware Middleware helps us run after return, and helps us to import the dispatch parameters. After completing the asynchronous operation, we use the data to send dispatch flat action, and continue to call reducer for subsequent operation.
const deleteAction = (index) => { return { type : 'DELETE_DATA', index } } const putdataAction = (task) => { return { type : 'PUT_DATA', task } } // Distribute non flat data. Middleware helps us run the layer inside const setdataAction = () => { return dispatch =>{ fetch('./data.json') .then( response => response.json()) .then(result => { dispatch(loaddataAction(result.result)) }) } } // Data type of distribution const loaddataAction = (data) =>{ return { type : 'SET_DATA', data } } export { deleteAction, putdataAction, setdataAction }
The reducer file under store
We have many consoles in reducer Log to help us take a more intuitive look at the calling process
const defaultState = {} const reducer = ( state = defaultState,action) => { console.log('---action---',action) switch(action.type){ case 'SET_DATA': console.log('----SET_DATA----',state) console.log('----SET_DATA----',action) return { ...state, list : action.data } case 'PUT_DATA': console.log('---PUT_DATA---' , state) console.log('---PUT_DATA---', action) return { ...state, list : [ ...state.list, { positionName : action.task, } ] } case 'DELETE_DATA': console.log('---DELETE_DATA---' , state) console.log('---DELETE_DATA---', action) let list1 = state.list.filter((v,i)=>{ return i!==action.index }) return { //... state is to prevent defaultState from having other attributes besides the list array ...state, list : list1 } default : return state } } export default reducer
design sketch
It can be seen that:
When no action is taken on the start page:
- The console prints -- -- action - {type: "@ @ redux / initi. 1.2. K.w.c"}, which is implemented by redux itself (the next blog);
- In the List file, first dispatch an asynchronous task setdataAction in componentDidMount;
- Then get the data of the fetch asynchronous request in the actioncreator file under the action of the Middleware, and then distribute a flat data type;
- The reducer file identifies the action After type, for action Data and returns an object
- The list file gets the list data in props for the initial page rendering
- In the console, the state is ----- set_ Data ---- {}, action ---- SET_DATA---- {type: "SET_DATA", data: Array(15)}
Then enter to add some tasks and delete any tasks. The logic is also the same. The reducer takes different actions Type to perform different operations.