初始化项目
使用 Create React App 进行项目初始化
npx create-react-app redux-demo
Redux工作流
要熟练的使用 Redux, 首先得熟悉 Redux 的工作流程。下面的图例完整的展示了 Redux 的全部工作流程,初次接触的时候只需要心中有个大概就行,后续工作使用 Redux 中,会发现就是反复在重复这个流程
分别介绍下各个流程的含义:
Action
一个用于描述要干什么动作的普通对象。其中 type 属性是必需的
{
type: 'ADD_TODO', // type字段必须
text: '早晨八点开始晨会'
}
Reducer
根据Action动作的不同,对仓库的数据做处理的一个纯函数。
纯函数的定义:
- 如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
- 该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变(mutation)。
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
default:
return state;
}
}
Reducer 内不修改 state 数据,只返回一个全新的数据状态
Store
仓库,负责存放状态(state),将 actions 与 reducers 联系起来的东西。
它的功能点有:
- 维持应用的 state;
- 提供
getState()方法获取 state; - 提供
dispatch(action)方法更新 state; - 通过
subscribe(listener)注册监听器; - 通过
subscribe(listener)返回的函数注销监听器。
从一个计数器开始
跟着官网按理简单的实现下 UI
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (
<div className="App">
<p>
数量: <span id="value">0</span>
<button id="increment">+</button>
<button id="decrement">-</button>
</p>
</div>
);
};
};
可以看到界面中有一个用来展示数量的容器和两个按钮,接下来我们就使用 Redux 来实现数量的加减与显示
Redux 初始化
首先我们需要创建一个文件夹 store, 并创建文件 index.js 用来初始化 Redux
第一步:安装 Redux
npm install redux --save
第二步:定义状态(state)
定义应用程序的初始状态值
即声明仓库中应该有哪些数据,初始值是什么
const initialState = {
value: 0
};
第三步:定义动作(action)
定义应用程序的状态修改动作
我们应该事先定义好我们应该可以运行状态可以进行哪些动作修改,这里计数器可以进行的分别是加和减
const counterIncrementedAction = () => ({ type: 'counter/incremented' })
const counterDecrementedAction = () => ({ type: 'counter/decremented' })
在这里我们分别声明了两个动作:加和减
第四步:创建 reducer
创建一个"reducer"函数来确定新的状态
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
default:
return state;
}
}
这里是定义动作的具体实现,也就是根据动作的不同返回不同的状态。(注意:这里还不能修改状态,只是返回一个全新的状态)
第五步:创建仓库(store)
Redux 提供了一个方法 createStore,可以使用这个方法进行仓库创建,他需要提供一个 Reducer
import { createStore } from 'redux'
// 其它逻辑...
const store = createStore(counterReducer);
export default store
注意:一个项目只能创建一个仓库(store)
至此我们完成了 Redux 的初始化,接下来我们开始使用它吧。
使用 Redux
回到刚开始的 UI 界面,我们需要把仓库中的状态展示在数量容器中,并实现加减运算
第一步:获取数据
首先我们需要获取到仓库中的状态,我们可以通过 getState 获取到仓库中的状态
import React, { Component } from 'react';
import store from "./store";
export default class App extends Component {
constructor(props) {
super(props);
this.state = store.getState()
}
render() {
return (
<div className="App">
<p>
数量: <span id="value">{this.state.value}</span>
<button id="increment">+</button>
<button id="decrement">-</button>
</p>
</div>
);
};
};
- 首先将仓库中的值全部取出,并在组件的构造函数中为
this.state赋初值 - 然后在数量容器中渲染该
value值
第二步:触发动作
触发动作需要使用 dispatch 方法来更新状态
// 将之前定义的动作引入并执行传入给 disapatch 方法
incremented() {
store.dispatch(counterIncrementedAction())
}
给按钮绑定点击事件
<button id="increment" onClick={this.incremented}>+</button>
这时点击后会进行更新状态
第三步:Redux DevTools
在浏览器调试时,可以使用谷歌插件 Redux DevTools 进行查看状态的变更
下载需要科学上网后在谷歌应用商店安装 Redux DevTools
下载完成后改造 store,在使用 createStore 方法时添加一个参数
const store = createStore(counterReducer, window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f);
现在可以使用插件调试发现我们点击 + 号,仓库状态可以成功 +1
第四步:订阅变更
在点击 + 号后仓库中状态成功更改了,但是视图中数据并未更改,此时还需要进行状态变更订阅
componentDidMount() {
store.subscribe(() => {
this.setState(store.getState())
})
}
在 componentDidMount 生命周期函数中进行监听订阅状态变更后更新当前组件的状态
至此一个的 Redux 使用案例就全部完成了
完整代码:
// APP组件, app.js
import React, { Component } from 'react';
import store, { counterIncrementedAction, counterDecrementedAction } from "./store";
export default class App extends Component {
constructor(props) {
super(props);
this.state = store.getState()
}
componentDidMount() {
store.subscribe(() => {
this.setState(store.getState())
})
}
incremented() {
store.dispatch(counterIncrementedAction())
}
decremented() {
store.dispatch(counterDecrementedAction())
}
render() {
return (
<div className="App">
<p>
数量: <span id="value">{this.state.value}</span>
<button id="increment" onClick={this.incremented}>+</button>
<button id="decrement" onClick={this.decremented}>-</button>
</p>
</div>
);
};
};
// store/index.js
import { createStore } from 'redux'
const initialState = {
value: 0
}
export const counterIncrementedAction = () => ({ type: 'counter/incremented' })
export const counterDecrementedAction = () => ({ type: 'counter/decremented' })
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
default:
return state;
}
}
const store = createStore(counterReducer, window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f);
export default store
Redux 高阶应用
就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。
但是,如果您希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux 中间件 的用武之地。它们扩展了 store,并允许您:
- dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
- 暂停、修改、延迟、替换或停止 dispatch 的 action
- 编写可以访问
dispatch和getState的额外代码 - 教
dispatch如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替
Redux 使用时在派发(dispatch)一个动作(action)给仓库(store)后,就完全交由仓库来变更状态(state),如果我们想要在仓库变更状态之前(reducer 之前)执行一些动作的话,可以使用中间件(middleware)
早些时候,我们看到了Redux 的同步数据流是什么样子。当我们引入异步逻辑时,我们添加了一个额外的步骤,中间件可以运行像 AJAX 请求这样的逻辑,然后 dispatch action。这使得异步数据流看起来像这样:
Redux Thunk
安装
npm install redux-thunk --save
插件配置
如果需要使用中间件,需要使用 redux 中的 applyMiddleware
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
// 一些其它逻辑...
const store = createStore(counterReducer, applyMiddleware(thunk));
如果需要配合使用 Redux Dev Tools 插件使用就需要额外配置一个增强函数,Redux 提供了一个 compose 方法来轻松帮我们实现一个增强函数
import { createStore , applyMiddleware ,compose } from 'redux'
import thunk from 'redux-thunk'
// 一些其它逻辑...
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose
const enhancer = composeEnhancers(applyMiddleware(thunk))
const store = createStore(counterReducer, enhancer);
Thunk 函数
将 thunk 中间件添加到 Redux store 后,它允许您将 thunk 函数 直接传递给 store.dispatch。调用 thunk 函数时总是将 (dispatch, getState) 作为它的参数,你可以根据需要在 thunk 中使用它们。
Thunks 通常还可以使用 action creator 再次 dispatch 普通的 action,比如 dispatch(counterIncrementedAction()):
export const counterIncrementedThunk = (dispatch, getState) => {
const stateBefore = getState()
console.log(`Counter before: ${stateBefore.value}`)
dispatch(counterIncrementedAction())
const stateAfter = getState()
console.log(`Counter after: ${stateAfter.value}`)
}
改造上述例子后:
<button onClick={this.incrementedThunk}>+</button>
incrementedThunk() {
store.dispatch(counterIncrementedThunk)
}
点击后同样可以实现数据的更新
为了与 dispatch 普通 action 对象保持一致,我们通常将它们写为 thunk action creators,它返回 thunk 函数。这些 action creator 可以接受可以在 thunk 中使用的参数。
export const counterIncrementedThunk = () => {
return (dispatch, getState) => {
const stateBefore = getState()
console.log(`Counter before: ${stateBefore.value}`)
dispatch(counterIncrementedAction())
const stateAfter = getState()
console.log(`Counter after: ${stateAfter.value}`)
}
}
使用时先执行函数
incrementedThunk() {
store.dispatch(counterIncrementedThunk())
}
“thunk” 这个词是一个编程术语,意思是 "一段做延迟工作的代码"
上述函数方法还是同步方法,下面开始实现异步编程
编写异步 Thunks
- 首先我们增加一个改变状态的动作
export const counterChangeAction = (value) => ({ type: 'counter/change', value })
- 动作逻辑实现
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/change":
return { ...state, value: action.value };
// ... 其它逻辑
}
}
- 编写 Thunk 异步函数
export const counterChangeThunk = () => {
return (dispatch) => {
fetch('/api/get').then(async res => {
if (res.ok) {
const { data } = await res.json()
dispatch(counterChangeAction(data))
}
})
}
}
- 使用 Thunk 异步函数
<button onClick={this.getValue}>获取远程数据</button>
getValue() {
store.dispatch(counterChangeThunk())
}
点击按钮后就可以用服务器返回的数据来更新状态
combineReducers
随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。
todos.js
// reducers/modules/todos.js
export default function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text])
default:
return state
}
}
counter.js
// reducers/modules/counter.js
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
reducers/index.js
import { combineReducers } from 'redux'
import todos from './modules/todos'
import counter from './modules/counter'
export default combineReducers({
todos,
counter
})
App.js
import { createStore } from 'redux'
import reducer from './reducers/index'
let store = createStore(reducer)
console.log(store.getState())
// {
// counter: 0,
// todos: []
// }
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
console.log(store.getState())
// {
// counter: 0,
// todos: [ 'Use Redux' ]
// }
type 集中管理
现在这种方式在使用过程中如果 type 不小心在否个地方写错了是很难发现,而且容易照成命名冲突,所以比较好的办法是集中一个文件管理项目中 types,其它地方进行引入使用。
// reducers/actionTypes.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
export const ADD_TODO = 'ADD_TODO'
还可以根据 type 模块不同进行模块划分
// reducers/actionTypes.js
export const TODO_ACTION_TYPE = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT'
}
export const COUNTER_ACTION_TYPE = {
ADD_TODO: 'ADD_TODO'
}
React-redux
Redux本身是一个独立的库,可以用于任何UI层或框架,包括React、Angular、Vue 和 vanilla JS。虽然Redux和React通常一起使用,但它们是相互独立的。
React-Redux是React的官方Redux UI绑定库。如果你同时使用Redux和React,你也应该使用React-Redux来绑定这两个库。
安装
npm install react-redux --save
Provider
用 Provider 节点包裹 <APP> 根节点,并将 store 作为 porps 值传递给 Provider ,之后在应用程序的每个组件都可以访问 Redux 存储了
import { Provider } from 'react-redux'
import store from './store'
const Root = ( <Provider store={store}> <App /> </Provider> )
connect 连接器
- 映射你需要使用的数据
const mapStateToProps = state => ({ value: state.counter.value })
- 使用连接器改造要导出的组件
export default connect(mapStateToProps,null)(App)
- 使用映射的数据
// 映射的数据会合并到组件 props 中
<span>{this.props.value}</span>
- 去除监听器
// componentDidMount() {
// store.subscribe(() => {
// this.setState({
// value: store.getState().counter.value
// })
// })
// }
去除监听器会发现之前代码的数据视图未更改,但是通过映射展示的数据视图是可以正常更新的
派发动作
- 定义 dispatch 映射
const dispatchToProps = dispatch => ({
incrementedThunk: () => {
dispatch(counterIncrementedThunk())
}
})
- 加入连接器
export default connect(mapStateToProps, dispatchToProps)(App)
- 使用映射的方法
<button onClick={this.props.incrementedThunk}>映射方法派发</button>
优化代码
优化完成后会发现该组件未无状态组件,那么可以改造成函数组件,优化性能
import React from 'react';
import { connect } from 'react-redux'
import {
counterChangeAction,
counterIncrementedAction,
counterDecrementedAction,
counterIncrementedThunk, counterChangeThunk
} from "./store/reducers/modules/counter";
const mapStateToProps = state => ({ value: state.counter.value })
const dispatchToProps = dispatch => ({
incremented: () => {
dispatch(counterIncrementedAction())
},
incrementedThunk: () => {
dispatch(counterIncrementedThunk())
},
decremented: () => {
dispatch(counterDecrementedAction())
},
valueChange: (e) => {
dispatch(counterChangeAction(e.target.value))
},
getValue() {
dispatch(counterChangeThunk())
},
})
const App = (props) => {
const { value, incremented, incrementedThunk, decremented, valueChange, getValue } = props
return (
<div className="App">
<p>
数量: <span id="value">{value}</span>
<button id="increment" onClick={incremented}>+</button>
<button id="decrement" onClick={decremented}>-</button>
</p>
<div>
<span>Action:</span>
<button onClick={incremented}>+</button>
<button onClick={decremented}>-</button>
</div>
<div>
<span>Thunk:</span>
<button onClick={incrementedThunk}>+</button>
<button onClick={decremented}>-</button>
</div>
<div>
<button onClick={getValue}>获取远程数据</button>
</div>
<div>
<input type="text" value={value} onChange={valueChange}/>
</div>
<p>
<span>{value}</span>
</p>
<div>
<button onClick={incrementedThunk}>映射方法派发</button>
</div>
</div>
)
}
export default connect(mapStateToProps, dispatchToProps)(App)
优化后会发现逻辑结构简单明了
Redux Toolkit
Redux Toolkit包旨在成为编写Redux逻辑的标准方式。它最初的创建是为了帮助解决关于 Redux 的三个常见问题:
- “配置 Redux 存储太复杂了”
- “我必须添加很多包才能让 Redux 做任何有用的事情”
- “Redux 需要太多样板代码”
使用 Create React
使用 React 和 Redux 启动新应用程序的推荐方法是使用官方 Redux+JS 模板或Redux+TS 模板来创建 React App,它利用了Redux Toolkit和 React Redux 与 React 组件的集成。
# Redux + Plain JS template
npx create-react-app my-app --template redux
# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript
安装
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
使用
Redux Toolkit 包括以下 API:
- configureStore(): 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的切片缩减器,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并允许使用 Redux DevTools Extension。
- createReducer():这使您可以为 case reducer 函数提供操作类型的查找表,而不是编写 switch 语句。此外,它自动使用immer库让您使用普通的可变代码编写更简单的不可变更新,例如state.todos[3].completed = true.
- createAction():为给定的动作类型字符串生成动作创建函数。该函数本身已toString()定义,因此可以使用它来代替类型常量。
- createSlice():接受reducer函数的对象,切片名称和初始状态值,并自动生成切片reducer,并带有相应的动作创建者和动作类型。
- createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk
- createEntityAdapter: 生成一组可重用的 reducer 和 selector 来管理 store 中的规范化数据
- 重新选择库中的createSelector实用程序,重新导出以方便使用。
回忆一下之前创建 Redux 需要做什么
- 定义初始状态(state)
const initialState = { value: 0 }; - 定义动作(action)
const action = () => ({ type: 'counter/incremented' }) - 创建 reducer
function (state = initialState, action) () { ... } - 配置中间件 Thunk
- 配置 Redux DevTools
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f - 创建仓库(store)
export default createStore(counterReducer)
看来要要完成 Redux 的搭建还是挺繁杂的,那看看 Redux Toolkit 需要怎么做呢
配置使用 Redux Toolkit
创建 reducer
通过 createSlice 方法可以一键完成之前的前三个步骤
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => { state.value += 1 },
decrement: state => { state.value -= 1 }
}
})
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
之前我们讲过在 reducer 函数需要返回一个全新状态,在这里就完全不需要担心这个问题了
创建仓库(store)
通过 configureStore 方法可以一键完成上述步骤的后三个步骤
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from './counter'
export default configureStore({
reducer: {
counter: counterReducer
}
})
组件中使用
除了前面讲述到的两种方式,还可以使用Hooks
- 导入
stroe直接使用 - 使用
connect连接器 - 使用
hooks函数
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment } from "../store/reducer";
export default () => {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>{count}</div>
<div>
<button onClick={() => dispatch(increment())}>+</button>
<button>-</button>
</div>
</div>
)
}
怎么样是不是简单明了许多,但是注意 hooks 函数在 class 组件中是不生效的
更多用法
还有许多其它用法可以参照官方文档Redux Toolkit 使用
最后
本文章大部分案例参照的是 Redux 中文网
还有部分是参照 技术胖的教程