Redux 与 React 集成实战:useSelector、useDispatch 与 Provider 的完整工作流
本文基于 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-redux
React 的核心理念是「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 函数。useDispatch hook 返回的就是 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 Header
6. 用 <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 时才 dispatch todos/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 示例 展示了同样的分工,且更贴近现代工程习惯:selector selectCount 与 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 的现代写法
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
