ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redux 与 React 集成实战:useSelector、useDispatch 与 Provider 的完整工作流

Redux 与 React 集成实战:useSelector、useDispatch 与 Provider 的完整工作流 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三大核心 APIuseSelector、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.jsimport 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 classNametodo-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.jsimport 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 typetext placeholderWhat 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在组件中派发 actionProvider 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 一个组件中使用多个 selectorFooter需要三块数据已完成 todo 数、当前 status 筛选值、当前选中的颜色分类。做法是在一个组件里多次调用useSelector而且这是推荐姿势——每次调用应返回尽可能小的状态切片src/features/footer/Footer.jsimport 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 classNamefooter div classNameactions h5Actions/h5 button classNamebuttonMark All Completed/button button classNamebuttonClear 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()包裹TodoListItemprops 没变就不重渲染让TodoList只读 ID 数组把id传给子项由每个TodoListItem自己按 ID 去 store 里取对象——这样只有真正变化的那一项才会重渲染。教程采用第二种方案src/features/todos/TodoList.jsimport 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 classNametodo-list{renderedListItems}/ul }子项则用参数化 selector 按 ID 取值并按 ID 派发 togglesrc/features/todos/TodoListItem.jsimport 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 classNameview{/* 省略其他渲染输出 */}/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 classNametodo-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安装useSelectorselector 接收整个 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),仅供参考
返回列表