preface
In the process of learning React, you will encounter Redux and React-Redux. If you are not familiar with Redux, you may wonder why there is Redux. First, let’s take a look at the use of Redux and some jokes in the process. Let’s see why react-Redux came into being
A story,
Before we start, here’s something to keep in mind
Redux is a well-known JavaScript state management container
In other words, Redux can be used with React, JS and Vue
1.1 Design Idea
Redux
Is to store the entire application state in a file calledstore
Where there is a state treestate tree
- Components are available through
store.dispatch
Distributing behavioraction
给store
.store
I’m not going to modify it directlystate
It is written by the userreducer
To generate newstate
And returns tostore
- Other components through subscription
store
To refresh your view
1.2 Three Principles
- The entire application has only one store, and its internal State tree stores the state of the entire application
- State is read-only and can only be modified by distributing actions. In order to describe how actions change state, we need to write a reducer pure function
- The design of a single data source facilitates communication between React components and enables unified state management
1.3 createStroe
Learn about the Redux API by writing a Counter Counter in action
1.3.1 store
// ./src/store.js
import {createStroe} from 'react'
function reudcer(){}
let store = createStore(reudcer)
Copy the code
The createStore method creates a store and needs to pass the reducer parameter (ps: below). The Store is an object and the following methods can be called
- Store.getstate () to get the latest state tree
- Store.dispatch (), dispatch action
- Store.subscribe (), the change of state in the subscription store
1.3.2 reducer
Reducer must be a pure function that accepts state and action. State is the old state and cannot be directly modified. New state needs to be generated and returned according to different action.type
// ./src/store.js
import {createStroe} from 'react'
export const ADD = 'ADD'
export const MINUS = 'MINUS'
function reducer (state = {count: 0}, action) {
console.log('action', action); // {type: 'xxx'}
switch(action.type) {
case ADD:
return {count: state.count + 1}
case MINUS:
return {count: state.count - 1}
default:
return state
}
}
let store = createStore(reudcer)
export default store
Copy the code
Note that in the code above, we set the initial value of state {count: 0}, and then use the exported store in the Counter component
1.3.3 getState, Dispatch,subscribe
// ./src/components/Counter.jsx
import React from 'react'
import store from '.. /store'
class Counter extends React.Component{
constructor(props){
super(props)
this.state = {
number: store.getState().count
}
}
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={()= > store.dispatch({type: 'ADD'})}>+</button>
<button onClick={()= > store.dispatch({type: 'MINUS'})}>-</button>
</div>}}export default Counter
Copy the code
In the Counter component, you can get the latest state through store.getState(). Click the button, and the action will be sent to the store through store.dispatch (ps: Please note that action is an object and must have type attribute), the current state will be passed to reducer within the store to generate a new state to update the state. Unfortunately, the number on the page has not changed
It can be seen that the Reducer function has received the action, and the state in the store has changed at this time, and the page is not updated because Counter did not subscribe to the state change in the store. Add the following code into the code
class Counter extends React.Component{
componentDidMount () {
this.unSubscribe = store.subscribe(() = > {
this.setState({
number: store.getState().count
})
})
}
componentWillUnmount () {
this.unSubscribe && this.unSubscribe()
}
}
Copy the code
Subscribe is implemented using store.subscribe, which takes a function that executes when state changes in store, and returns a function that unsubscribes.
At this point, the Counter component is basically implemented. Maybe some of you noticed that the console output came out when your app first loaded
action {type: "@@redux/INIT1.s.m.m.c.n"}
Copy the code
Action {type: “@@redux/ init1.s.m.m.c.n “}
For those familiar with the publish-subscribe model, Redux uses publish-subscribe internally. Next, let’s try to implement a crude version of Redux
1.3.4 Implement createStroe by handwriting
function createStore(reducer){
let state
const listeners = []
// Return the latest state
function getState () {
return state
}
Distributed / / action
function dispatch(action){
state = reducer(state, action)
listeners.forEach(listener= > listener())
}
// Subscribe, return unsubscribe function
function subscribe(listener){
listeners.push(listener)
return function () {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)}}// Get the default state value
dispatch({type: "@@redux/INIT1.s.m.m.c.n"})
// Return store, an object
return {
getState,
dispatch,
subscribe
}
}
export default createStore
Copy the code
Through testing, our humble version of Redux has implemented the functionality of the Counter component
1.4 bindActionCreators
1.4.1 Principle and Use
In the Counter component, we dispatch actions directly using store.dispatch
<button onClick={() = > store.dispatch({type: 'ADD'})}>+</button>
<button onClick={()= > store.dispatch({type: 'MINUS'})}>-</button>
Copy the code
The drawback is that redux provides the bindActionCreators function, in which store.dispatch is repeated many times, and action.type is easy to miswrite and difficult to find. Bind the function that dispatches the action to store.dispatch
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '.. /store'
// The add function returns action, so it can be called actionCreator
function add() {
return {type: 'ADD'}}function minus() {
return {type: 'MINUS'}}const bindAdd = bindActionCreators(add, store.dispatch)
const bindMinus = bindActionCreators(minus, store.dispatch)
class Counter extends React.Component{
// ...
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={bindAdd}>+</button>
<button onClick={bindMinus}>-</button>
</div>}}export default Counter
Copy the code
In fact, the bindActionCreators logic can be pulled out of the 👆 code into a separate file that can be used in other components. Meanwhile, the above code does not make sense because it requires manual binding for each function, so bindActionCreators supports passing in objects and wrapping all actionCreator functions as objects
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '.. /store'
// The add function returns action, so it can be called actionCreator
function add() {
return {type: 'ADD'}}function minus() {
return {type: 'MINUS'}}const actions = {add, minus}
const bindActions = bindActionCreators(actions, store.dispatch)
class Counter extends React.Component{
// ...
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={ bindActions.add} >+</button>
<button onClick={ bindActions.minus} >-</button>
</div>}}export default Counter
Copy the code
1.4.2 Handwriting implementation
function bindActionCreators (actionCreater, dispatch) {
ActionCreater can be a function/object
if (typeof actionCreater === 'function') {
return function (. args) {
returndispatch(actionCreater(... args)) } }else {
let bindActionCreaters = {}
Object.keys(actionCreater).forEach(key= > {
bindActionCreaters[key] = function (. args) {
returndispatch(actionCreater(... args)) } })return bindActionCreaters
}
}
export default bindActionCreaters
Copy the code
1.5 combineReducers
1.5.1 Principle and Usage
When an application contains multiple modules, it is not reasonable to put the state of all modules on the reducer. It is better to divide each module by reducer, and each module has its own reducer and action. Finally, the reducer is merged into a large reducer through combineReducers in Redux
// src\store\reducers\index.js
import {combineReducers} from 'redux';
import counter1 from './counterReducer1';
import counter2 from './counterReducer2';
export default combineReducers({
x: counter1,
y: counter2
});
// src/store/reducers/counterReducer1.js
import * as types from '.. /action-types';
export default function (state= {count: 0},action){
switch(action.type){
case types.ADD1:
return state.count + 1;
case types.MINUS1:
return state.count - 1;
default:
returnstate; }}// src/store/reducers/counterReducer2.js
import * as types from '.. /action-types';
export default function (state= {count: 0},action){
switch(action.type){
case types.ADD2:
return state.count + 1;
case types.MINUS2:
return state.count - 1;
default:
returnstate; }}Copy the code
The combineReducers method accepts an object, and the property key can be set arbitrarily. The property value corresponds to the Reducer function of each module and returns a final reducer method after merging.
After the reducer merge, the state tree in the store will also be divided according to modules
store.getState()
{
x: {count: 0}
y: {count: 0}}Copy the code
Thus, in the component, the use of state needs to be modified to look like this
import store from '.. /store';
export default class Counter extends Component {
constructor(props) {
super(props);
this.state = {
value: store.getState().x.count
}
}
/ /...
}
Copy the code
When action is distributed in the component, it will be passed to the function returned by combineReducers. In this function, the reducer of each module will be called to generate its own new state. Finally, after all the states are merged, Update the state in store
1.5.2 Handwriting implementation
function combineReducers(reducers){
// Return the reducer function after the merge
return function (state, action){
const nextState = {}
Object.keys(reducers).forEach(key= > {
nextState[key] = reducers[key](state[key], action)
})
return nextState
}
}
Copy the code
It can be seen that the reducer function of each module will execute actions that are mainly distributed
1.6 summary
When you use stores in React, you need to manually import store files and subscribe to store state changes. This is not appropriate
Second, the React – story
2.1 Principle and use
The React-Redux provides a Provider component. Through the Provider component, you can transfer stores to its children and grandchildren without importing each component manually
// ./src/index.js
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>.document.getElementById('root'));Copy the code
In the descendant Counter1, you can use react-redux to provide a connect function to associate the store with the props of the Counter1 component
import React from 'react'
import { connect } from 'react-redux'
import action from '.. /store/actions/Counter1'
class Counter1 extends React.Component{
render () {
return <div>
<p>{ this.props.count }</p>
<button onClick={ this.props.add} >+</button>
<button onClick={ this.props.minus} >-</button>
</div>}}const mapStateToProps = state= > state
constmapDispatchToProps = { ... action }export default connect(mapStateToProps, mapDispatchToProps)(Counter1)
Copy the code
As can be seen from the above code, properties or methods of Counter1 are accessed through props. It is completely possible to convert Counter1 into function components (stateless components), which are wrapped by a container component (stateful components). All connect(mapStateToProps, mapDispatchToProps)(Counter1) returns a container component. How to write a React-redux
2.2 Handwriting Implementation
To pass stores across components, react-Redux uses the React Context API internally
Create a ReactReduxContext context object
// src/react-redux/Context.js
import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext
Copy the code
In the Proveider component, you need to use the Provider component provided in the ReactReduxContext object
// src/react-redux/Provider.js
import React from 'react'
import {ReactReduxContext} from './Context'
class Provider extends React.Component{
constructor(props) {
super(props)
}
render () {
return <ReactReduxContext.Provider value={{ store: this.props.store}} >
{this.props.children}
</ReactReduxContext.Provider>}}export default Provider
Copy the code
The connect method, which takes mapStateToProps and mapDispatchToProps, returns a function that takes a custom component (such as Counter1) and returns the final container component
// src/react-redux/connect.js
import React from 'react'
import {bindActionCreators} from 'redux'
import {ReactReduxContext} from './Context'
function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
// Returns the final container component
return class extends React.Component{
static contextType = ReactReduxContext
constructor(props, context){
super(props)
this.state = mapStateToProps(context.store.getState())
}
shouldComponentUpdate() {
if (this.state === mapStateToProps(this.context.store.getState())) {
return false;
}
return true;
}
componentDidMount () {
this.unsubscribe = this.context.subscribe(() = > {
this.setState(mapStateToProps(this.context.store.getState()))
})
}
componentWillUnmount (){
this.unsubscribe && this.unsubscribe()
}
render(){
const actions = bindActionCreators(
mapDispatchToProps,
this.context.store.dispatch
)
return <WrappedComponent {. this.state} {. this.props} {. actions} / >}}}}export default connect
Copy the code
As you can see, in the Connect method, bindActionCreators bundled the Action with store.dispatch, and state changes in the subscription Store, which we did with redux only. The React component needs to be written manually. Fortunately, react-Redux now does that for us
Third, summary
Why redux and React-Redux should be introduced in react applications