Redux 与 React 集成实战:useSelector、useDispatch 与 Provider 的完整工作流
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
本文基于 Redux 官方仓库 Fundamentals 教程第 5 篇「UI and React」整理并扩充:讲解 Redux store 如何与任意 UI 层集成的通用五步模式,以及 React 场景中react-redux三大核心 API(useSelector、useDispatch、<Provider>)的具体用法、重渲染机制与列表性能优化方案(按 ID 选择 +shallowEqual)。读完后你将能在自己的 React 项目中完成 Redux 数据流的完整接线,并理解每条规则背后的 store 源码依据。
1. 从 Store 到 UI:这一篇解决什么问题
在 Fundamentals 教程的第 4 篇(Store)中,我们已经掌握了如何创建 store、dispatch action、读取 state,以及 enhancer 与 middleware 如何扩展 store 的能力。第 5 篇要回答的问题是:store 已经能读写数据了,UI 层如何跟上这些变化?
本篇以教程的 todo 应用为载体,覆盖两块内容:
- Redux store 与任意 UI 层(不限 React)集成的通用步骤;
- Redux 与React配合的官方做法,即 React-Redux 的 hooks API。
注意:本篇及整个 Fundamentals 教程刻意采用「传统写法」(手写 action、手写 reducer 调用
useSelector),目的是讲清原理;现代项目推荐配合 Redux Toolkit 使用,官方教程 Essentials 系列(essentials/part-1)展示的就是「现代 Redux + React-Redux hooks」的组合。
2. Redux 与任意 UI 层集成的五步通用模式
Redux 本身只是一个独立的 JS 库。正如仓库的 core 源码 所示,它不依赖任何 UI 框架——你可以用 React、Vue、Angular、jQuery 甚至原生 JavaScript 来使用它,且客户端、服务端都适用。
无论用哪种 UI 技术,「store + UI」的集成都遵循同样几个步骤:
- 创建一个 Redux store;
- 订阅 store 的更新(
store.subscribe); - 在订阅回调中:
- 用
store.getState()获取当前 state; - 从中提取出本块 UI 需要的数据;
- 用这份数据更新 UI;
- 用
- 必要时用初始 state 先把 UI 渲染出来;
- 在 UI 输入事件里 dispatch Redux action。
教程用第 1 篇的 counter 应用演示了这套模式(见 part-1-overview):
// 1) 用 `createStore` 创建 Redux store const store = Redux.createStore(counterReducer) // 2) 订阅:数据变化时重新渲染 store.subscribe(render) // 这里的"用户界面"是 HTML 里的一行文字 const valueEl = document.getElementById('value') // 3) 订阅回调执行时: function render() { // 3.1) 读取当前 state const state = store.getState() // 3.2) 提取需要的数据 const newValue = state.value.toString() // 3.3) 更新 UI valueEl.innerHTML = newValue } // 4) 用初始 state 渲染 UI render() // 5) 根据 UI 输入 dispatch action document.getElementById('increment').addEventListener('click', function () { store.dispatch({ type: 'counter/incremented' }) })这套模式在本仓库中有真实的原生实现可以对照:examples/counter-vanilla/index.html 就是一个零依赖的 vanilla counter——render()调用store.getState()后写入#value元素,render()先执行一次做初始渲染,再store.subscribe(render),按钮点击事件里store.dispatch({ type: 'INCREMENT' }),与上面五步一一对应。
2.1 源码印证:五步模式背后的三个 API
这五步之所以成立,是因为 store 恰好提供了这三个 API。从 src/createStore.ts 的实现可以看到它们的约束条件,这些约束直接解释了 React 绑定层为什么要那样设计:
getState()(src/createStore.ts#L166-L176):返回当前 state。注意它在isDispatching期间被调用会直接抛错——reducer 执行中不允许再从 store 读 state,只能把 state 作为参数传下去。subscribe(listener)(src/createStore.ts#L201-L243):注册变更监听器,每次 dispatch 后回调。源码注释特别说明:订阅列表在每次dispatch()前做快照,dispatch 过程中增删订阅不影响当前这一次派发,但会影响下一次。返回的闭包即 unsubscribe。dispatch(action)(src/createStore.ts#L270-L309):唯一的改状态入口。源码里能看到三重校验:action 必须是 plain object、type不能是undefined且必须是字符串;执行 reducer 前后维护isDispatching标志,reducer 中再 dispatch 会抛 "Reducers may not dispatch actions.";最后遍历 listener 快照逐一回调,并返回该 action。
理解了这三点就能明白:所谓「绑定库」(binding library)做的无非是——替你调用subscribe、在回调里调用getState()、比较新旧值决定 UI 是否更新,从而省掉你手写这部分样板代码。
3. React 场景:安装 react-redux 并设计组件树
React-Redux 是官方的 React 绑定库,独立于 redux 核心包,需要单独安装:
npm install react-reduxReact 的核心理念是「UI 是 state 的函数」,而 Redux 恰好负责管理 state 并响应 action 更新它——两者天然互补,这也是 Redux 专门为 React 优化的原因。教程从业务需求出发,先设计出最小组件树:
<App>:根组件,渲染其余所有组件<Header>:包含「新增 todo」文本输入框和「全部完成」复选框<TodoList>:按筛选结果渲染当前可见的 todo 列表<TodoListItem>:单个 todo,含切换完成状态的复选框和颜色分类选择器
<Footer>:显示未完成 todo 数量,以及按完成状态和颜色分类筛选的控制项
组件如何划分没有唯一答案(比如<Footer>可以再拆成<CompletedTodos>、<StatusFilter>、<ColorFilters>),大型场景下拆得更细往往更好。教程在此假设读者已掌握 React 基础,直接聚焦 React-Redux 的用法。
4. 用useSelector从 store 读取状态
4.1 selector:从整个 state 中取值或派生值的函数
React-Redux 提供了一组自定义 hooks,其中最常用的是useSelector,它让组件能够读取 store 中的数据。它接受一个函数参数,称为selector(选择器):selector 接收整个store state,读出一个值并返回。它既可以返回原始数据,也可以返回派生值:
// 取出 todo 数组 const selectTodos = state => state.todos// 派生值:统计已完成 todo 的数量 const selectTotalCompletedTodos = state => { const completedTodos = state.todos.filter(todo => todo.completed) return completedTodos.length }在<TodoList>中调用它,就能拿到 todo 数组并循环渲染子项(src/features/todos/TodoList.js):
import React from 'react' import { useSelector } from 'react-redux' import TodoListItem from './TodoListItem' const selectTodos = state => state.todos const TodoList = () => { const todos = useSelector(selectTodos) // `todos` 是数组,可以直接 map const renderedListItems = todos.map(todo => { return <TodoListItem key={todo.id} todo={todo} /> }) return <ul className="todo-list">{renderedListItems}</ul> } export default TodoList首次渲染时,useSelector会用整个 state 对象调用selectTodos,并把返回值交给组件,于是todos就是 store 里那份state.todos数组。selector 也可以直接内联书写:const todos = useSelector(state => state.todos)。
4.2 自动订阅与强制重渲染:不必每个组件都手写 subscribe
关键问题是:dispatch 一个{ type: 'todos/todoAdded' }之后,reducer 更新了 state,组件凭什么知道自己该重渲染?
理论上可以在每个组件里手写store.subscribe(),但会非常重复。useSelector会自动替组件订阅 store:每次有 action 被 dispatch,它就立即重新执行一次 selector;如果 selector 的返回值与上次不同,就强制该组件用新数据重渲染。这正好对应 2.1 节中subscribe/getState的源码机制——React-Redux 只是把这套手动流程封装进了 hook。
4.3 重要陷阱:===引用比较导致的频繁重渲染
必须记住的一条规则:
useSelector用严格===比较两次 selector 的结果;只要结果是「新引用」,组件就会重渲染。
因此在 selector 里返回新建的引用(新数组、新对象)会导致组件在每一次action dispatch 后都重渲染,即使数据其实没变。反例:
// 坏例子:每次 dispatch 都返回一个新数组引用 const selectTodoDescriptions = state => { // 这创建了一个新的数组引用! return state.todos.map(todo => todo.text) }教程在此先给出问题,解法留到后文「按 ID 选择 +shallowEqual」(见第 6.3 节)以及 第 7 篇 Standard Redux Patterns 中讲解的「记忆化 selector」来解决。
5. 用useDispatch派发 action
组件中如何触发状态变更?组件文件里拿不到 store 实例,需要单独拿到dispatch函数。useDispatchhook 返回的就是 store 的dispatch方法(其实现本质上就是return store.dispatch)。
在<Header>中,用受控输入框收集文本,用户按下 Enter 键时 dispatch{ type: 'todos/todoAdded' }(src/features/header/Header.js):
import React, { useState } from 'react' import { useDispatch } from 'react-redux' const Header = () => { const [text, setText] = useState('') const dispatch = useDispatch() const handleChange = e => setText(e.target.value) const handleKeyDown = e => { const trimmedText = e.target.value.trim() // 用户按下 Enter 键: if (e.key === 'Enter' && trimmedText) { // 派发 "todo 新增" action,携带这段文本 dispatch({ type: 'todos/todoAdded', payload: trimmedText }) // 清空输入框 setText('') } } return ( <input type="text" placeholder="What needs to be done?" autoFocus={true} value={text} onChange={handleChange} onKeyDown={handleKeyDown} /> ) } export default Header6. 用<Provider>把 store 交给组件树
组件能读 state、能 dispatch 了,但 hooks 只是 JS 函数,不会自己知道该用哪个 store。答案:把<Provider>渲染在整棵<App>外层,并通过 prop 传入 store,这样应用内所有组件都能访问到这个 store。在主入口src/index.js中添加:
import React from 'react' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' import App from './App' import store from './store' const root = createRoot(document.getElementById('root')) root.render( // 用 `<Provider>` 包住整个 `<App>`, // 并把 Redux store 作为 prop 传给 `<Provider>` <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> )至此,React-Redux 的三大件齐了:
useSelector:在组件中读取 store 数据useDispatch:在组件中派发 action<Provider store={store}>:包住整个<App>,让组件树都能访问 store
本仓库的 counter 示例 用的正是这套结构:examples/counter/src/index.js#L12-L18 中<Provider store={store}>包裹<App />,store 由 examples/counter/src/app/store.js 里的configureStore创建。
7. React-Redux 实战模式
7.1 全局状态、组件状态与表单
「是不是所有状态都要放进 store?」答案是否。判断准则:
跨组件共享的全局状态放 Redux store;只有一处用到的状态留在组件本地(
useState)。
<Header>里的输入文本就是典型:只有这个组件用它,放进 store 毫无收益,留在useState里即可。同理,一个isDropdownOpen这种布尔开关也不该进 store。拿不准时,可以问自己这六个问题(教程给出的经验法则):
- 应用的其他部分关心这份数据吗?
- 需要基于它进一步派生数据吗?
- 同一份数据是否驱动多个组件?
- 有没有价值把它恢复到某个历史时间点(时间旅行调试)?
- 需要缓存它吗(已存在则不再重新请求)?
- 希望热重载组件时它还保持一致吗(组件内部状态可能在热替换时丢失)?
由此引出表单的一般原则:多数表单状态不应存进 Redux。编辑过程中数据留在表单组件里,用户提交完成时再 dispatch action 更新 store——<Header>正是这么做的(只有按 Enter 时才 dispatchtodos/todoAdded)。仓库中的 examples/counter/src/features/counter/Counter.js#L16 也是同一思路:incrementAmount输入框的值用useState管理,只在点击按钮时才dispatch(incrementByAmount(...))。
7.2 一个组件中使用多个 selector
<Footer>需要三块数据:已完成 todo 数、当前 status 筛选值、当前选中的颜色分类。做法是在一个组件里多次调用useSelector,而且这是推荐姿势——每次调用应返回尽可能小的状态切片(src/features/footer/Footer.js):
import React from 'react' import { useSelector } from 'react-redux' import { availableColors, capitalize } from '../filters/colors' import { StatusFilters } from '../filters/filtersSlice' // 省略其他 footer 子组件 const Footer = () => { const todosRemaining = useSelector(state => { const uncompletedTodos = state.todos.filter(todo => !todo.completed) return uncompletedTodos.length }) const { status, colors } = useSelector(state => state.filters) // 省略占位变更处理函数 return ( <footer className="footer"> <div className="actions"> <h5>Actions</h5> <button className="button">Mark All Completed</button> <button className="button">Clear Completed</button> </div> <RemainingTodos count={todosRemaining} /> <StatusFilter value={status} onChange={onStatusChange} /> <ColorFilters value={colors} onChange={onColorChange} /> </footer> ) } export default Footer这里完成数用派生 selector 计算,两个筛选值都住在state.filters切片里,组件恰好两者都要,于是整块选中。
7.3 按 ID 选择列表项数据 +shallowEqual防抖重渲染
<TodoList>直接把整个state.todos数组读出来、把 todo 对象作为 prop 传给每个<TodoListItem>。问题在于引用链:
- 修改任意一个 todo,会复制该 todo 和
state.todos数组,两者都是新引用; useSelector检测到新引用,就强制<TodoList>重渲染;- React 默认递归重渲染所有子组件,于是所有
<TodoListItem>都会重渲染,哪怕绝大多数没变。
组件重渲染本身不是坏事(React 靠它判断是否需要更新 DOM),但列表很大时「什么都没变却全部重渲染」会明显变慢。优化有两条路:
- 用
React.memo()包裹<TodoListItem>,props 没变就不重渲染; - 让
<TodoList>只读 ID 数组,把id传给子项,由每个<TodoListItem>自己按 ID 去 store 里取对象——这样只有真正变化的那一项才会重渲染。
教程采用第二种方案(src/features/todos/TodoList.js):
import React from 'react' import { useSelector } from 'react-redux' import TodoListItem from './TodoListItem' const selectTodoIds = state => state.todos.map(todo => todo.id) const TodoList = () => { const todoIds = useSelector(selectTodoIds) const renderedListItems = todoIds.map(todoId => { return <TodoListItem key={todoId} id={todoId} /> }) return <ul className="todo-list">{renderedListItems}</ul> }子项则用参数化 selector 按 ID 取值,并按 ID 派发 toggle(src/features/todos/TodoListItem.js):
import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { availableColors, capitalize } from '../filters/colors' const selectTodoById = (state, todoId) => { return state.todos.find(todo => todo.id === todoId) } // 解构 props.id,只需要 ID 值 const TodoListItem = ({ id }) => { // 同时传入 state 和 ID 值调用 selector const todo = useSelector(state => selectTodoById(state, id)) const { text, completed, color } = todo const dispatch = useDispatch() const handleCompletedChanged = () => { dispatch({ type: 'todos/todoToggled', payload: todo.id }) } // 省略其他变更处理与渲染内容 return ( <li> <div className="view">{/* 省略其他渲染输出 */}</div> </li> ) } export default TodoListItem但别忘了 4.3 节的坑:selectTodoIds返回的仍是每次map出的新数组。切换某个 todo 的完成状态时,ID 列表内容没变,容器引用却变了,<TodoList>仍会无谓重渲染。解法是给useSelector传第二个参数——比较函数:比较函数接收新旧两个值,返回true视为相同、不触发重渲染。React-Redux 内置了shallowEqual,逐项比较数组内部元素:
import React from 'react' import { useSelector, shallowEqual } from 'react-redux' import TodoListItem from './TodoListItem' const selectTodoIds = state => state.todos.map(todo => todo.id) const TodoList = () => { const todoIds = useSelector(selectTodoIds, shallowEqual) const renderedListItems = todoIds.map(todoId => { return <TodoListItem key={todoId} id={todoId} /> }) return <ul className="todo-list">{renderedListItems}</ul> }效果:toggle 某个 todo 时,ID 列表被判定为「相同」,<TodoList>不再重渲染;只有持有所变 todo 的那一个<TodoListItem>拿到新对象并重渲染,其余子项引用不变、纹丝不动。这正是「每次useSelector返回最小切片」原则的直接收益。更系统的解法(记忆化 selector)将在 第 7 篇 展开。
7.4 对照仓库实例:selector 可以定义在 slice 文件里
本仓库 examples/counter 示例 展示了同样的分工,且更贴近现代工程习惯:selectorselectCount与 reducer 同放一个文件(counterSlice.js#L59-L62):
// selector 也可以内联在使用处定义, // 例如:useSelector((state: RootState) => state.counter.value) export const selectCount = state => state.counter.value组件里一行const count = useSelector(selectCount)配合const dispatch = useDispatch()完成接线(Counter.js#L13-L16),与本篇 todo 示例的模式完全一致。
8. 本篇小结
现在 todo 应用已经完整跑通:创建 store → 用<Provider>交给 React 树 → 组件内useSelector读数据、useDispatch派发动作。核心结论回顾:
- Redux store 可用于任意 UI 层:UI 代码永远是「订阅 store → 取最新 state → 重绘自己」;
- React-Redux 是 React 的官方绑定库,独立包
react-redux安装; useSelector:selector 接收整个 state 并返回值;hook 自动订阅 store,每次 dispatch 后重跑 selector,结果变化(===比较)才触发组件重渲染;在 selector 中返回新引用会导致每次 dispatch 都重渲染;useDispatch:返回真实的store.dispatch,组件内随时dispatch(action);<Provider store={store}>包住整个<App>,是组件树访问 store 的唯一入口。
动手练习(教程布置):在<TodoListItem>中用useDispatch补上「修改颜色分类」和「删除 todo」两个 action 的派发;在<Footer>中补上「全部标记完成」「清除已完成」「切换筛选值」的派发。筛选的完整实现见 第 7 篇 Standard Redux Patterns。
下一篇 第 6 篇:Async Logic 将讨论超时、HTTP 请求等异步逻辑如何融入 Redux 数据流。
9. 延伸阅读(仓库内)
- 第 4 篇:Store —— store 内部机制、enhancer 与 middleware,本篇五步模式的前置知识
- 第 7 篇:Standard Redux Patterns —— 记忆化 selector 与筛选派生数据的完整解法
- src/createStore.ts ——
getState/subscribe/dispatch的完整实现(含 reducer 执行期的调用约束),理解所有绑定层行为的第一手材料 - examples/counter-vanilla —— 零依赖的原生 JS 五步集成实例
- examples/counter —— 带
<Provider>、useSelector、useDispatch的完整 React 工程示例 - Essentials 教程 —— Redux Toolkit + React-Redux hooks 的现代写法
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考