Redux is a state container for JavaScript that provides predictable state management. Redux is generally used with React, but it can also be used in Vue, which is said to be based on Redux.
In complex projects, it is unrealistic to communicate between components by passing props layer by layer, which requires global unified state management, as well as the need to know the process of state change to facilitate debugging.
To facilitate tracking, redux requires that the state is read-only in use, and the data in the state must be updated by action, and the update function is called reducer. reducer must be a pure function to facilitate tracking.
Basic Usage
It’s very simple to use, subscribe to store updates, change the store by dispatch, call the reducer function to update the store, and trigger the subscribe callback function when the update is complete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
const redux = require('redux')
const initialState = {
name: 'default'
}
// 不需要直接修改state
function reducer(state = initialState, action) {
switch (action.type) {
case 'CHANGE_NAME':
return { ...state, name: action.name }
default:
return state
}
}
const store = redux.createStore(reducer)
console.log(store.getState())
store.subscribe(() => {
console.log('store 发生了改变')
console.log(store.getState())
})
store.dispatch({
type: 'CHANGE_NAME',
name: '张三'
})
|
For the above code, the general standard way of encapsulation is as follows. I feel it would be neater to encapsulate it into a Class for convenience
1
2
3
4
|
store/index.js # 导出store
store/reducer.js # 定义 initialState 和 reducer
store/actionCreators.js # 定义action
store/constants.js # 定义actionName
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class Store {
constructor() {
this.initialState = {
name: 'default'
}
this.store = redux.createStore(this.reducer)
}
subscribe = callback => this.store.subscribe(callback)
getState = () => this.store.getState()
dispatch = action => this.store.dispatch(action)
reducer = (state = this.initialState, action) => {
switch (action.type) {
case 'CHANGE_NAME':
return { ...state, name: action.name }
default:
return state
}
}
changeName(name) {
this.dispatch({
type: 'CHANGE_NAME',
name: name
})
}
}
const store = new Store()
console.log(store.getState())
store.subscribe(() => {
console.log('store 发生了改变')
console.log(store.getState())
})
store.changeName('张三')
|
Splitting for business
If you define all the actions in one reducer function, it will be bloated, you can split it into multiple reducer functions according to the business, and then merge them into one reducer
If all the reducers of a module are executed under multiple modules, there will be a problem when the action name is the same name, refer to the module of vuex, you can define the namespace yourself, for example {type: 'order/CHANGE_MONEY'}
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
const initialState = {}
const initialOrder = { money: 100 }
function orderReducer(state = initialOrder, action) {
switch (action.type) {
case 'CHANGE_MONEY':
return { ...state, money: action.value }
default:
return state
}
}
function reducer(state = initialState, action) {
return {
order: orderReducer(state.order, action)
}
}
const store = redux.createStore(reducer)
export { store }
|
Using in React
Updates to components in React are triggered by setState. Using the above, it is easy to write the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class App2 extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
count: store.getState().count
}
this.unsubscribe = null
}
componentDidMount() {
// redux 更新后调用setState
// 避免重复渲染,要么重写shouldComponentUpdate要么使用PureComponent
this.unsubscribe = store.subscribe(() => this.setState({ ...this.state, count: store.getState().count }))
}
componentWillUnmount() {
this.unsubscribe()
}
render(h) {
return (
<div>
<span>APP2:</span>
<span>{this.state.count}</span>
</div>
)
}
}
|
The above code shows that the steps to use Redux in each component are the same, so each component is written in this way, which may seem redundant.
This is actually problematic because if you use the entire store directly, updating other states will lead to unnecessary rendering of the Wrap component, and due to the shallow comparison of PureComponent, the component will not update when the data is defined at a deeper level. A good practice is to take the required properties from the store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function Wrap(C) {
return class extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
$store: store.getState()
}
this.unsubscribe = null
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
return this.setState({ $store: store.getState() })
})
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
return <C {...this.props} {...this.state}></C>
}
}
}
|
1
2
3
4
5
6
7
8
9
10
|
let WrapedApp2 = Wrap(App2)
render(h) {
let WrapedApp2 = Wrap(App2)
return (
<div className="App">
<WrapedApp2></WrapedApp2>
</div>
)
}
|
react-redux
The above eliminates code redundancy by encapsulating higher-order components, but it will lead to repeated rendering, so higher-order components also need to pass in those properties that need to be associated with the state. The above Wrap also has an external dependency store, which needs to be imported every time a Wrap component is implemented.
The official react-redux provides two methods to indicate which state properties are exposed and which action methods are exposed, and these properties are attached to the component’s props
1
2
3
4
5
6
7
|
function mapStateToProps(state) {
return { ...state }
}
function mapDispatchToProps(dispatch, ownProps) {
return { changeName: changeName }
}
|
1
2
3
4
5
6
7
8
9
10
11
|
import { connect, Provider } from 'react-redux'
render(h) {
let WrapedApp2 = connect(mapStateToProps, mapDispatchToProps)(App2)
return (
<Provider store={store}>
<div className="App">
<WrapedApp2></WrapedApp2>
</div>
</Provider>
)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class App2 extends React.PureComponent {
change = () => {
this.props.changeName('Hello ' + this.props.name)
}
render(h) {
return (
<div onClick={this.change}>
<span>APP2:</span>
<span>{this.props.name}</span>
</div>
)
}
}
|
redux middleware
redux has middleware functionality that extends the code between dispatch and reducer. For example, the middleware can be used to print out the values before and after each dispatch
1
2
3
4
5
6
7
8
9
10
11
12
|
function logger(store) {
return next => {
return action => {
console.log('before', store.getState())
console.log('do', action)
let result = next(action)
console.log('after', store.getState())
return result
}
}
}
const store = redux.createStore(reducer, redux.applyMiddleware(logger))
|
redux-thunk
redux-thunk is a middleware, by default dispatch(action) must be passed in an object, with redux-thunk middleware you can make the action a function. redux-thunk automatically adds dispatch, getState to this function, generally to reduce the need to call the network request in the This is generally used to reduce the need to call the network request in the component’s componentDidMount and then submit it to the redux state, which can be done directly with redux-thunk.
1
|
npm install --save redux-thunk
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import thunk from 'redux-thunk'
const store = redux.createStore(reducer, redux.applyMiddleware(thunk, logger))
function mockAjax(param) {
return (dispatch, getState) =>
// ajax url param
new Promise((resolve, reject) => {
resolve('xxx')
}).then(data => {
dispatch({
type: 'CHANGE_NAME',
name: data
})
})
}
function loadData() {
store.dispatch(mockAjax({ param: 'xxx' }))
}
|
redux-saga
Like redux-thunk, it is used to handle asynchronous, but it uses the ES6 generator syntax, which is more elegant in comparison
redux-devtools
Its usage is a bit different than other middleware, it is not a middleware, but an enhancement.
It requires the Chrome plugin Redux DevTools, which is similar to the Vue DevTools plugin for viewing the state of vuex.
1
2
3
4
5
6
7
|
let middlewares = redux.applyMiddleware(logger)
if (process.env.NODE_ENV === 'development' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
middlewares = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(middlewares)
}
const store = redux.createStore(reducer, middlewares)
|
ImmutableJS
In order to ensure that state can be safely tracked, it is always emphasized in using react not to modify the properties in state directly, but to return a new object.
For objects, we generally use es6’s ...
shallow copy to return a new object, and slice(0)
for arrays first. To ensure immutability and convenient and efficient manipulation of objects, ImmutableJS has emerged.
The following APIs are commonly used, and there are many others that include all operations on collections
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 深层转化为immutable对象
immutable.fromJS
immutable.toJS
// 常用数据结构,可以用于浅层转换js数据到immutable对象
List Map Set ...
// 兼容es6语法
forEach map filter ....
// 深度比较相等
is
// 获取值 getIn(['user', 'age'])
get getIn
// 是否有key
has hasIn
set setIn
delete deleteIn
size push clear includes first last delete
|
1
2
3
4
5
6
7
8
9
|
let store = {
user: {
age: 18
}
}
// true
console.log(immutable.Map(store).get('user') === store.user)
// false
console.log(immutable.fromJS(store).get('user') === store.user)
|
Using redux in Hooks
Starting with Redux 7.1, Hooks are provided so that we no longer need to write connect and the corresponding mapping functions
useSelector: Mapping the state to the component
useDispatch: introduces the dispatch function
Note that the second argument to useSelector is similar to shouldComponentUpdatel in that it is used to determine whether to re-render, and returns true to not re-render
By default the refEquality
comparison is used. Something like const { count } = useSelector(state => ({ count: state.count }))
, which returns a new object every time, would require defining its own equality comparison function
1
2
3
|
var refEquality = function refEquality(a, b) {
return a === b;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function App3() {
const age = useSelector(state => state.age)
// 每次返回新对象refEquality比较为不等,所以修改其他属性也会导致重新渲染
const { count } = useSelector(state => ({ count: state.count }))
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch({ type: 'UPDATE_COUNT', value: count + 1 })}>update count</button>
<div>{age}</div>
<div>{count}</div>
</div>
)
}
|