写在前面
本文代码通过 create-react-app
脚手架进行搭建,所有的代码均可直接复制运行。
代码位置:react-redux-demo
本文主要讲解了 Redux 和 React-Redux 的使用,详细的概念以及设计思想请看 Redux 中文官网
- 安装
create-react-app
脚手架
# npm
npm install create-react-app -g
#yarn
yarn add create-react-app -g
- 创建
React
项目
create-react-app my-app
- 安装
redux
npm install redux
# or
yarn add redux
用法详解
Action
描述更新对象
是把数据从组件传到 store
的载体,是修改 store
数据的唯一来源。
是一个普通的 javascript
对象,必须包含一个 type
属性,用来通知 reducer
这个 action
需要做的操作类型。
比如:
{
type: 'ADD',
payload: 1
}
通过 store.dispatch(action)
将 action
传给 store
Reducer
执行更新函数
描述 store
数据如何更新的纯函数,接受两个参数
-
state
:store
中的state
值,可以给state
设置初始值 -
action
:通过store.dispatch(action)
传递的action
对象
通过 action
的 type
类型来判断如何更新 state
数据
比如:
function reducer(state = 0, { type, payload }) {
switch (type) {
case "ADD":
return state + payload;
case "DELETE":
return state - payload;
default:
return state;
}
}
Store
将 action
和 reducer
联系到一起的对象,具有以下职责
- 维持应用的
state
- 提供
getState()
方法获取state
- 提供
dispatch(action)
方法更新state
- 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器
store
的创建方式
const store = createStore(reducer[, prevState, ehancer]);
实现一个简单的 React
计数器
- 通过
store.dispatch(action)
通知数据更新 - 通过
store
获取state
数据 - 编写
reducer
实现数据更新
store/index.js
import { createStore } from "redux";
// 创建 reducer 函数 ,更新 state 数据
const reducer = function(state = 0, { type }) {
switch (type) {
case "INCREMENT":
return ++state;
case "DECREMENT":
return --state;
default:
return state;
}
};
// 创建 store
const store = createStore(reducer);
export default store;
App.js
import React, { useEffect, useReducer, useCallback } from "react";
import store from "./store";
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch({ type: "INCREMENT" }),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch({ type: "DECREMENT" }),
[]
);
return (
<div className="App">
<h1>Hello Redux</h1>
{/* 获取当前 state 值 */}
<p>count: {store.getState()}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</div>
);
}
export default App;
这个时候,计数器已经实现了,点击 increment
或者 decrement
按钮,会更新界面上的数据
那假如说,此时我们可能会处理多个业务场景,比如一个是计数器,一个是 TodoList
,会有两个 reducer
,这个时候该如何创建呢?请看下一个 API
combineReducers
合并 reducer
是一个高阶函数,作用是将多个 reducer
函数按照合并生成一个 reducer
函数。
接受一个对象,返回一个 reducer
函数。对象的键可以设置任意属性名,对象的值是对应的 reducer
函数。
在使用
store
中的state
值时,state
中的对应的属性名就是之前传给combineReducers
方法的对象的属性名。
比如:
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
获取 state
时:
const state = store.getState();
// state: { count: xxx, todos: xxx }
我们在上面的例子中再加一个展示 TodoList
的功能
store/index.js
import { createStore, combineReducers } from "redux";
// 创建 counterReducer 函数 ,更新 state 数据
const counterReducer = function(state = 0, { type }) {
switch (type) {
case "INCREMENT":
return ++state;
case "DECREMENT":
return --state;
default:
return state;
}
};
// 创建 todoReducer 函数,更新 state 数据
const todoReducer = function(state = [], { type, payload }) {
switch (type) {
case "INIT":
return payload;
case "ADD":
state.push(payload);
return [...state];
default:
return state;
}
};
// 合并 reducer
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
// 创建 store
const store = createStore(reducer);
export default store;
App.js
import React, { useEffect, useReducer, useCallback, useState } from "react";
import store from "./store";
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch({ type: "INCREMENT" }),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch({ type: "DECREMENT" }),
[]
);
const add = useCallback(() => {
if (value) {
// 分发 action
store.dispatch({ type: "ADD", payload: value });
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
至此,计数器和 TodoList
的功能都已经实现了
我们现在一直用的都是 redux
自己的功能,如果我想使用一些插件该怎么做呢,比如我想使用 logger
插件打印一些日志,请看下一个 API
applyMiddleware
应用插件
使用 applyMiddleware
可以应用插件,扩展 redux
功能。
applyMiddleware
是一个函数,接受多个参数值,返回一个高阶函数供 createStore
使用。
const ehancer = applyMiddleware(middleware1[, middleware2, middleware3, ...]);
下面我们以 redux-logger
插件为例,使用 applyMiddleware
:
安装 redux-logger
npm install redux-logger -D
# or
yarn add redux-logger -D
store/index.js
// ...
// 合并 reducer
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
// 应用插件
const ehancer = applyMiddleware(logger);
// 创建 store
const store = createStore(reducer, ehancer);
export default store;
上面我们修改数据的时候一直都是在同步状态下进行,那如果现在有一个副作用操作,需要异步执行完成才能进行 state
更新,又该怎么做呢?就要用到 react-thunk
插件了
react-thunk
这个插件把 store
的 dispatch
方法做了一层封装,可以接受一个函数作为 action
。
当判断当前 action
是一个函数的时候,会自动执行,并将 dispatch
作为参数传给我们。
安装 react-thunk
npm install react-thunk -D
# or
yarn add react-thunk -D
下面我们看看 react-thunk
的用法以及使用场景
还是在刚才的例子上,我们想要在组件加载完成之后对 TodoList
添加一些初始值,这个过程是一个异步过程
将 react-thunk
插件应用到 store
中去
store/index.js
import thunk from "react-thunk";
// ...
// 应用插件
const ehancer = applyMiddleware(thunk, logger);
// 创建 store
const store = createStore(reducer, ehancer);
export default store;
App.js
// ...
useEffect(() => {
// 派发一个异步 action,是一个函数
store.dispatch(dispatch => {
setTimeout(() => {
dispatch({ type: "INIT", payload: ["吃饭", "睡觉", "敲代码"] });
}, 1000);
});
}, []);
// ...
我们现在分发 action
的时候,都是直接 dispatch
一个对象,代码少的情况下还好,多的话可能就比较复杂,还要和 reducer
中的 type
对应,所以写起来比较麻烦,下面我们介绍一个概念:ActionCreator
ActionCreator
: action
创建函数
这不是一个 API
或者方法,只是一种思想和实现。就是通过调用一个函数生成一个对应的 action
,在需要的时候我们直接调用这个函数,进行 dispatch
就可以了
比如:
const addTodo = todo => ({ type: "ADD", payload: todo });
这种写法我们 dispatch
的时候不用考虑 type
,也不用写键值对,只要传入正确的参数,就可以了。
下面我们就把刚才我们写的例子修改一下,使用这种 ActionCreator
的思想去编写 action
store/index.js
中添加 actionCreator
并导出
// ...
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const initTodos = todos => ({ type: "INIT", payload: todos });
const addTodo = todo => ({ type: "ADD", payload: todo });
// 异步 action,执行完成之后调用同步 action
const getAsyncTodos = () => dispatch =>
setTimeout(() => dispatch(initTodos(["吃饭", "睡觉", "写代码"])), 1000);
export const actionCreators = { increment, decrement, getAsyncTodos, addTodo };
App.js
中 dispatch
中调用 actionCreator
import React, { useEffect, useReducer, useCallback, useState } from "react";
import store, { actionCreators } from "./store";
const {
increment as inc,
decrement as dec,
getAsyncTodos,
addTodo
} = actionCreators;
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
useEffect(() => {
// 派发一个异步 action,是一个函数
store.dispatch(getAsyncTodos());
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch(inc()),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch(dec()),
[]
);
const add = useCallback(() => {
if (value) {
// 分发 action
store.dispatch(addTodo(value));
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
但是这个时候,感觉还是有点麻烦,每次触发 store
更新都要使用 dispatch(actionCreator(args))
才行,能不能直接调用方法,就能触发 store 更新呢。
当然也可以了,redux
为我们提供了一个 bindActionCreators
函数
bindActionCreators
这个函数是将 dispatch
绑定到了 actionCreator
方法上,之后只要我们执行 actionCreator
就会触发 store
更新了,不用每次都 dispacth
了。
接受两个参数:
actionCreators
:是一个对象,对象的属性名可以任意命名,属性值是对应的actionCreator
方法dispatch
:store
中的dipatch
属性
返回一个新的对象,对象的属性名是刚才传入的 actionCreators
中的属性名,属性值时包装后的方法,执行即可触发 store
更新
比如:
const finalActions = bindActionCreators({
increment: () => ({ type: 'INCREMENT }),
decrement: () => ({ type: 'DECREMENT })
}, dispatch)
// finalActions: { increment, decrement }
下面我们将 App.js
中的代码进行一波优化,看看最后的效果
import React, { useEffect, useReducer, useCallback, useState } from "react";
import { bindActionCreators } from "redux";
import store, { actionCreators } from "./store";
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = bindActionCreators(
actionCreators,
store.dispatch
);
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, []);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
有没有发现,到目前为止,数据变化时,我们更新 React 组件还是通过自己去添加 subscribe 订阅,一个组件还好,那在项目开发的过程中,每个组件都这么写,岂不是太麻烦了。所以下面我们介绍
react-redux
的使用,可以帮我们解决这个问题
结合 Reatc 使用的正确姿势:react-redux
- 安装
npm install react-redux
# or
yarn add react-redux
-
常规用法
Provider
: 使用Provider
标签包裹根组件,将store
作为属性传入,后续的子组件才能获取到 store 中的state
和dispatch
connect
:返回一个高阶组件,用来连接React
组件与Redux store
,返回一个新的已与Redux store
连接的组件类。
-
hooks
用法useDispatch
:返回一个dispatch
对象useSelector
:接受一个函数,将函数的返回值返回出来
Provider
使用
根目录下 index.js
文件
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store";
import "./index.css";
ReactDOM.render(
<React.StrictMode>
{/* 使用 Provider 标签包裹住根组件,并将 store 作为参数传入 */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
connect
的使用
API
:connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])
connect
的用法相对复杂一些,接受四个参数,返回的是一个高阶组件。用来连接当前组件和 Redux store
。
-
mapStateToProps
:函数类型,接受两个参数:state
和ownProps
(当前组件的props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会与组件的props
合并(state[, ownProps]) => ({ count: state.count, todoList: state.todos })
-
mapDispatchToProps
:object | 函数- 不传递这个参数时,
dispatch
会默认挂载到组件的的props
中 - 传递
object
类型时,会把object
中的属性值使用dispatch
包装后,与组件的props
合并
{ increment: () => ({ type: "INCREMENT" }), decrement: () => ({ type: "DECREMENT" }), }
-
对象的属性值都必须是
ActionCreator
-
dispatch
不会再挂载到组件的props
中 -
传递函数类型时,接收两个参数:
dispatch
和ownProps
(当前组件的props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会和组件的props
合并
(state[, ownProps]) => ({ dispatch, increment: dispatch({ type: "INCREMENT" }), decrement: dispatch({ type: "DECREMENT" }) })
- 不传递这个参数时,
-
mergeProps
:(很少使用) 函数类型。如果指定了这个参数,mapStateToProps()
与mapDispatchToProps()
的执行结果和组件自身的props
将传入到这个回调函数中。该回调函数返回的对象将作为props
传递到被包装的组件中。你也许可以用这个回调函数,根据组件的props
来筛选部分的state
数据,或者把props
中的某个特定变量与ActionCreator
绑定在一起。如果你省略这个参数,默认情况下组件的props
返回Object.assign({}, ownProps, stateProps, dispatchProps)
的结果mergeProps(stateProps, dispatchProps, ownProps): props
-
options
context?: Object
pure?: boolean
areStatesEqual?: Function
areOwnPropsEqual?: Function
areStatePropsEqual?: Function
areMergedPropsEqual?: Function
forwardRef?: boolean
下面 改写一下 App.js 中 redux 的用法
mapDispatchToProps
参数不传时
import React, { useEffect, useCallback, useState, useMemo } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { actionCreators } from "./store";
function App({ count, todos, dispatch }) {
const [value, setValue] = useState("");
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
() => bindActionCreators(actionCreators, dispatch),
[dispatch]
);
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 也会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// 第二个参数没有传递,dispatch 默认会挂载到组件的 props 中
export default connect(mapStateToProps)(App);
mapDispatchToProps
参数为对象时
import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
function App({ count, todos, increment, decrement, getAsyncTodos, addTodo }) {
const [value, setValue] = useState("");
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// actionCreators 中的 actionCreator 会被 dispatch 进行包装,之后合并到组建的 props 中去
const mapDispatchToProps = { ...actionCreators };
export default connect(mapStateToProps, mapDispatchToProps)(App);
mapDispatchToProps
参数为函数时
import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { bindActionCreators } from "redux";
function App({
count,
todos,
increment,
decrement,
getAsyncTodos,
addTodo,
dispatch
}) {
const [value, setValue] = useState("");
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// mapDispatchToProps 为函数时,actionCreators 中的 actionCreator 需要自己处理,返回的对象会被合并到组件的 props 中去
const mapDispatchToProps = dispatch => ({
dispatch,
...bindActionCreators(actionCreators, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
- 如果不需要更新 store 中的数据,则不需要传 mapDispatchToProps 参数
- 如果不需要自己控制 dispatch,则传递 ActionCreators 对象即可
- 如果需要自己完全控制,则传递一个回调函数
虽然上面使用
connect
是在class
组件中,但是在函数组件中依然适用。
useDispatch
和 useSelector
的使用
上面我们在组件中使用的是 connect
,但是在现在这个 hooks
盛行的时代,怎么能只有高阶组件呢,所以下面我们来探究一下 useDispatch
和 useSelector
的用法。
改写 App.js
文件
import React, { useEffect, useCallback, useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "./store";
function App() {
const [value, setValue] = useState("");
// 从 useDispatch 中获取 dispatch
const dispatch = useDispatch();
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
() => bindActionCreators(actionCreators, dispatch),
[dispatch]
);
// 通过 useSelector 获取需要用到 state 值
const { count, todos } = useSelector(({ count, todos }) => ({
count,
todos
}));
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
使用 hooks
方式改写之后,感觉简洁了不少,数据来源也很清晰。至于用 connect
还是 hook
的方式,可以根据情况自己选择。
总结
-
Redux
由Action、Reducer、Store
组成- 通过
Reducer
创建Store
- 通过
store.dispatch(action)
触发更新函数reducer
- 通过
reducer
更新数据 - 数据更新触发订阅
subscribe
- 通过
-
通过
combineReducers
可以合并多个reducer
-
通过
applyMiddleware
可以使用插件 -
通过
bindActionCreators
可以将ActionCreator
转化成dispatch
包装后的ActionCreator
源码地址
如果觉得有所帮助,欢迎 Star!