尧图精选

React Redux Hooks 完全指南:useSelector、useDispatch 与 useStore 实战详解

🕒 发布时间:2026/9/20 9:06:57 📁 来源:尧图网络
前端【免费下载链接】react-reduxOfficial React bindings for Redux项目地址https://gitcode.com/gh_mirrors/re/react-redux点击查看免费下载React Redux 在 v7.1.0 中引入了一套全新的 Hooks API为函数组件提供订阅 Redux store、派发 action 的能力作为传统connect()高阶组件的替代方案。本文以 React Redux 官方 v7.1 版 Hooks 文档为核心骨架结合当前仓库中的真实源码实现系统讲解useSelector、useDispatch、useStore三大核心 Hook 的用法、相等性比较机制、与connect的差异、内存化选择器的最佳实践、stale props 与 zombie children 等边界情况以及可直接复制使用的自定义 Hook 配方。读完本文你将掌握在 React 函数组件中以更简洁、更利于 TypeScript 的方式接入 Redux 的完整方案。为什么使用 Hooks APIReact 的 Hooks 机制让函数组件能够使用局部组件状态、执行副作用等能力。React Redux 在此基础上提供了一套 Hook API作为现有connect()高阶组件的替代方案这些 API 允许你订阅 Redux store 并派发 action而无需用connect()包裹组件。官方明确建议将 React-Redux Hooks API 作为 React 组件中的默认接入方式。现有connectAPI 仍然可用并会持续获得支持但 Hooks API 更简单且与 TypeScript 配合得更好。这些 Hooks 最早在 v7.1.0 中引入从此成为 React Redux 与 Redux 交互的首选路径。从当前仓库源码看Hooks 相关的公开导出集中在 src/exports.tscreateDispatchHook/useDispatch、createSelectorHook/useSelector、createStoreHook/useStore均从这里对外暴露同时导出的还有ReactReduxContext、Provider、shallowEqual、batch等配套设施。在 React Redux 应用中使用 Hooks与connect()一样首先需要用Provider组件包裹整个应用使 store 在整个组件树中可用const store createStore(rootReducer) ReactDOM.render( Provider store{store} App / /Provider, document.getElementById(root), )在 React 18 中使用ReactDOM.createRoot(...).render(...)的写法同样适用见当前版本文档 docs/api/hooks.md。之后你就可以在任意函数组件中导入并使用下列 React Redux Hooks API。从源码层面看Provider的核心职责是向 context 中注入 store 与订阅实例src/components/Provider.tsx 通过React.useMemo构造contextValue包含store、subscription以及可选的getServerState并渲染Context.Provider value{contextValue}。而 context 本体定义在 src/components/Context.ts名为ReactReduxContext其值结构ReactReduxContextValue包含store、subscription与getServerState字段见 src/components/Context.ts。useSelector()函数签名const result: any useSelector(selector: Function, equalityFn?: Function)useSelector允许你通过一个选择器selector函数从 Redux store state 中提取数据。需要注意选择器函数应当是纯函数pure因为它可能在任意时间点被执行多次。选择器在概念上近似于传给connect的mapStateToProps参数可参考 connect-extracting-data-with-mapStateToProps。选择器只接收整个 Redux store state 作为唯一参数。每当函数组件渲染时选择器都会被执行除非其引用与上一次渲染相同此时 Hook 可直接返回缓存结果而无需重跑选择器。useSelector()还会订阅 Redux store每当有 action 被派发时重新运行你的选择器。与 mapState 函数的差异useSelector()的选择器与mapState函数存在以下几点区别选择器可以返回任意类型的值而不只是对象其返回值将直接作为useSelector()Hook 的返回值。当 action 被派发时useSelector()会对上一次的选择器结果与当前结果做引用比较如果不同则强制组件重新渲染相同则不渲染。选择器函数不接收ownProps参数。不过 props 可以通过闭包closure使用见下文示例或通过柯里化选择器使用。使用内存化memoizing选择器时需格外小心详见下文示例。useSelector()默认使用严格的引用相等性检查而不是浅相等shallow equality。关于在 selector 中使用 props 可能带来的边界问题详见后文 Usage Warnings 一节。你可以在同一个函数组件中多次调用useSelector()每次调用都会创建对 Redux store 的独立订阅。得益于 React Redux v7 的 React 更新批处理batching行为一个 action 即使导致同一组件中的多个useSelector()返回新值通常也只会触发一次重新渲染。从当前仓库源码看这一机制的具体实现位于 src/hooks/useSelector.tsHook 内部通过useSyncExternalStoreWithSelector完成订阅 store 外部状态读取 选择器结果相等性比较其中订阅函数来自subscription.addNestedSub状态来源为store.getState服务端渲染时使用getServerState。默认的相等性比较函数refEquality定义为(a, b) a b见 src/hooks/useSelector.ts这正是文档所述默认严格引用比较的源码依据。相等性比较与更新机制当函数组件渲染时提供的选择器函数会被调用其结果从useSelector()返回若选择器引用与上次渲染相同Hook 可能直接返回缓存结果而不重跑选择器。然而当 action 被派发到 Redux store 时useSelector()只有当选择器结果与上次结果看起来不同时才会强制重新渲染。自 v7.1.0-alpha.5 起默认比较是严格的引用比较。这与connect()不同——后者对mapState调用的结果使用浅相等检查来判断是否需要重渲染。这一差异对你如何使用useSelector()有若干重要影响使用mapState时所有字段都被组合在一个返回对象中返回对象是否是新的引用并不重要——connect()只逐个比较字段。而使用useSelector()时每次返回新对象都默认会强制重新渲染。如果你希望从 store 中获取多个值可以多次调用useSelector()每次调用返回单个字段值使用Reselect 或类似库创建内存化选择器使其返回包含多个值的对象但仅当其中一个值变化时才返回新对象将 React Redux 的shallowEqual函数作为equalityFn参数传给useSelector()import { shallowEqual, useSelector } from react-redux // later const selectedData useSelector(selectorReturningObject, shallowEqual)可选的比较函数也支持使用类似 Lodash 的_.isEqual()或 Immutable.js 的比较能力。shallowEqual的实现在 src/utils/shallowEqual.ts先通过类似Object.is的is函数做引用级比较再比较键数量最后逐个键做浅层比较Object.prototype.hasOwnProperty校验 is比较。useSelector示例基础用法import React from react import { useSelector } from react-redux export const CounterComponent () { const counter useSelector((state) state.counter) return div{counter}/div }通过闭包使用 props 决定提取内容import React from react import { useSelector } from react-redux export const TodoListItem (props) { const todo useSelector((state) state.todos[props.id]) return div{todo.text}/div }使用内存化memoizing选择器如上所示当useSelector与内联选择器一起使用时组件每次渲染都会创建新的选择器实例。只要选择器不维护任何内部状态这没有问题。但内存化选择器例如通过 reselect 的createSelector创建带有内部状态因此使用时必须小心。下面给出内存化选择器的典型使用场景。当选择器只依赖 state 时只需确保它在组件外部声明使每次渲染使用同一个选择器实例import React from react import { useSelector } from react-redux import { createSelector } from reselect const selectNumCompletedTodos createSelector( (state) state.todos, (todos) todos.filter((todo) todo.completed).length, ) export const CompletedTodosCounter () { const numCompletedTodos useSelector(selectNumCompletedTodos) return div{numCompletedTodos}/div } export const App () { return ( spanNumber of completed todos:/span CompletedTodosCounter / / ) }如果选择器依赖组件 props但只会被单个组件的单个实例使用同样适用import React from react import { useSelector } from react-redux import { createSelector } from reselect const selectCompletedTodosCount createSelector( (state) state.todos, (_, completed) completed, (todos, completed) todos.filter((todo) todo.completed completed).length, ) export const CompletedTodosCount ({ completed }) { const matchingCount useSelector((state) selectCompletedTodosCount(state, completed), ) return div{matchingCount}/div } export const App () { return ( spanNumber of done todos:/span CompletedTodosCount completed{true} / / ) }然而当选择器在多个组件实例中使用且依赖组件 props 时你需要确保每个组件实例拥有自己的选择器实例否则多个实例会共享同一份内存缓存导致结果串用。典型做法是用useMemo按实例创建import React, { useMemo } from react import { useSelector } from react-redux import { createSelector } from reselect const makeSelectCompletedTodosCount () createSelector( (state) state.todos, (_, completed) completed, (todos, completed) todos.filter((todo) todo.completed completed).length, ) export const CompletedTodosCount ({ completed }) { const selectCompletedTodosCount useMemo(makeSelectCompletedTodosCount, []) const matchingCount useSelector((state) selectCompletedTodosCount(state, completed), ) return div{matchingCount}/div } export const App () { return ( spanNumber of done todos:/span CompletedTodosCount completed{true} / spanNumber of unfinished todos:/span CompletedTodosCount completed{false} / / ) }useDispatch()函数签名const dispatch useDispatch()这个 Hook 返回 Redux store 中dispatch函数的引用你可以用它按需派发 action。示例import React from react import { useDispatch } from react-redux export const CounterComponent ({ value }) { const dispatch useDispatch() return ( div span{value}/span button onClick{() dispatch({ type: increment-counter })} Increment counter /button /div ) }当把使用dispatch的回调传给子组件时你有时可能想用useCallback对其内存化。如果子组件正尝试使用React.memo()或类似手段优化渲染行为这可以避免因回调引用变化导致子组件不必要的重新渲染import React, { useCallback } from react import { useDispatch } from react-redux export const CounterComponent ({ value }) { const dispatch useDispatch() const incrementCounter useCallback( () dispatch({ type: increment-counter }), [dispatch], ) return ( div span{value}/span MyIncrementButton onIncrement{incrementCounter} / /div ) } export const MyIncrementButton React.memo(({ onIncrement }) ( button onClick{onIncrement}Increment counter/button ))从源码看useDispatch的实现非常轻量src/hooks/useDispatch.ts 内部通过useStore()获取 store然后直接返回store.dispatch。这也是dispatch 引用稳定这一特性的直接来源——只要传入Provider的是同一个 store 实例dispatch引用就不会变化。dispatch 引用的稳定性与依赖数组dispatch函数引用在向Provider传入同一个 store 实例期间是稳定的。通常情况下应用中的 store 实例不会改变。然而React Hooks 的 lint 规则并不知道dispatch应该是稳定的会警告你应当把dispatch变量加入useEffect和useCallback的依赖数组。最简单的解决方案就是照做export const Todos() () { const dispatch useDispatch() useEffect(() { dispatch(fetchTodos()) // Safe to add dispatch to the dependencies array }, [dispatch]) }把dispatch加入依赖数组是安全的因为它的引用在 store 不变时保持稳定不会引起不必要的副作用重跑。useStore()函数签名const store useStore()这个 Hook 返回传入Provider组件的同一个 Redux store 引用。这个 Hook 不应该被频繁使用——请优先把useSelector()作为主要选择。不过在少数确实需要访问 store 的场景下它很有用例如替换 reducers。示例import React from react import { useStore } from react-redux export const CounterComponent ({ value }) { const store useStore() // EXAMPLE ONLY! Do not do this in a real app. // The component will not automatically update if the store state changes return div{store.getState()}/div }注意示例中的用法直接渲染store.getState()仅是演示真实应用中不要这样做——组件不会在 store state 变化时自动更新。更稳妥的做法是在事件回调等非渲染时机读取store.getState()或干脆把这类逻辑放进 thunk 中。从源码看useStore的实现同样简单直接src/hooks/useStore.ts 从useReduxContext()中取出store并返回而useReduxContext内部通过React.useContext(context)读取 context 值并在开发模式下当 context 值为空时抛出could not find react-redux context value; please ensure the component is wrapped in aProvider错误见 src/hooks/useReduxContext.ts——这也解释了为什么所有 Hooks 都必须在Provider包裹的组件树内使用。自定义 ContextProvider组件允许通过contextprop 指定一个备用 context。这在构建复杂可复用组件时非常有用可以避免你的 store 与使用方应用中的任何 Redux store 发生冲突。要通过 Hooks API 访问备用 context请使用 Hook 工厂函数creator functionsimport React from react import { Provider, createStoreHook, createDispatchHook, createSelectorHook, } from react-redux const MyContext React.createContext(null) // Export your custom hooks if you wish to use them in other files. export const useStore createStoreHook(MyContext) export const useDispatch createDispatchHook(MyContext) export const useSelector createSelectorHook(MyContext) const myStore createStore(rootReducer) export function MyProvider({ children }) { return ( Provider context{MyContext} store{myStore} {children} /Provider ) }从源码看这三个工厂函数默认都绑定到ReactReduxContext当传入自定义 context 时会创建对应的上下文 Hook见 src/hooks/useSelector.ts、src/hooks/useDispatch.ts、src/hooks/useStore.ts。Provider的contextprop 类型定义在 src/components/Provider.tsx注释明确要求使用自定义 context 时需把初始值设为null且若 Hooks 未被子 Provider 覆盖会报错。使用警告Usage WarningsReact-Redux Hooks API 自 v7.1.0 发布以来已可投入生产使用官方推荐将其作为组件中的默认接入方式。不过仍存在少量边界情况以下是需要了解的细节。Stale Props 与 Zombie ChildrenReact Redux 实现中最棘手的方面之一是确保当mapStateToProps定义为(state, ownProps)时每次都以最新的 props 调用它。直到版本 4都有涉及边界情况的反复出现的 bug 报告例如对数据刚被删除的列表项执行mapState函数时抛出错误。从版本 5 开始React Redux 尝试保证与ownProps的一致性。在版本 7 中这是通过connect()内部的一个自定义Subscription类实现的它形成嵌套层级结构这确保了组件树中较底层的已连接组件只会在最近的已连接祖先组件更新之后才收到 store 更新通知。然而这依赖于每个connect()实例覆盖内部 React context 的一部分提供自己唯一的Subscription实例来形成嵌套并用新的 context 值渲染ReactReduxContext.Provider。使用 Hooks 时没有渲染 context provider 的方式因此也没有嵌套的订阅层级。正因为如此stale props 和 zombie child 问题在依赖 Hooks 而非connect()的应用中可能重新出现。Subscription的嵌套机制在 src/utils/Subscription.ts 中有完整实现createSubscription(store, parentSub)支持传入父级订阅addNestedSub在首次被调用时通过trySubscribe()向上订阅有parentSub时调用parentSub.addNestedSub(handleChangeWrapper)否则直接store.subscribe(...)见 src/utils/Subscription.ts从而实现祖先先于后代更新的保证。具体来说stale props指以下任何情况选择器函数依赖本组件的 props 来提取数据父组件会因为某个 action 重新渲染并向下传递新的 props但本组件的选择器函数在本组件有机会用新 props 重新渲染之前就执行了。根据使用了哪些 props 以及当前 store state 是什么这可能导致选择器返回错误数据甚至抛出异常。Zombie child特指以下情况多个嵌套的已连接组件在第一次渲染时挂载导致子组件在父组件之前订阅 store某个 action 被派发删除了 store 中的数据例如一个 todo 项父组件会因此停止渲染该子组件但因为子组件先订阅它的订阅在父组件停止渲染它之前运行。当它基于 props 从 store 读取值时该数据已不存在如果提取逻辑不够小心可能抛出异常。useSelector()试图通过捕获所有因 store 更新而执行选择器时抛出的错误但不会捕获渲染期间执行选择器时的错误来处理这一问题。当发生错误时组件会被强制渲染此时选择器会再次执行。只要选择器是纯函数、并且你不依赖选择器抛错这种方法就能奏效。如果你希望自行处理这一问题以下是完全避免这些问题的可选方案不要在 selector 函数中依赖 props 来提取数据在你确实依赖 props且这些 props 可能随时间变化或提取的数据可能基于会被删除的项时尝试防御性地编写选择器。不要直接写state.todos[props.id].name——先读取state.todos[props.id]在读取todo.name前确认它存在因为connect会向 context provider 添加必要的Subscription并延迟子订阅的求值直到已连接组件重新渲染所以在使用useSelector的组件上方放置一个已连接组件只要该已连接组件因同一次 store 更新而重新渲染就能防止这些问题。性能如前所述默认情况下useSelector()会在 action 派发后运行选择器时对选中的值做引用相等性比较只有选中值变化时才导致组件重新渲染。然而与connect()不同useSelector()无法阻止组件因父组件重新渲染而重新渲染即使组件的 props 没有变化。如果需要进一步优化性能可以考虑用React.memo()包裹函数组件const CounterComponent ({ name }) { const counter useSelector(state state.counter) return ( div {name}: {counter} /div ) } export const MemoizedCounterComponent React.memo(CounterComponent)Hooks Recipes可直接复制的自定义 Hook官方从最初的 alpha 版本中精简了 Hooks API聚焦于更小的一组 API 原语。但你也许仍希望在自有应用中使用当初尝试过的某些方案。以下示例可以直接复制粘贴到你的代码库中。Recipe:useActions()这个 Hook 曾存在于最初的 alpha 版本中但在v7.1.0-alpha.4中被移除。移除理由是基于绑定 action creators在 Hooks 使用场景中并不那么有用而且会造成过多概念负担与语法复杂度。更好的做法是在组件中调用useDispatch获取dispatch引用然后在回调与副作用中按需手动调用dispatch(someActionCreator())。你也可以在自己的代码中使用 Redux 的bindActionCreators函数绑定 action creators或者手动绑定const boundAddTodo (text) dispatch(addTodo(text))。如果你仍然希望使用这个 Hook下面是一个支持传入单个函数、数组或对象形式的 action creators 的可复制版本import { bindActionCreators } from redux import { useDispatch } from react-redux import { useMemo } from react export function useActions(actions, deps) { const dispatch useDispatch() return useMemo( () { if (Array.isArray(actions)) { return actions.map(a bindActionCreators(a, dispatch)) } return bindActionCreators(actions, dispatch) }, deps ? [dispatch, ...deps] : [dispatch] ) }Recipe:useShallowEqualSelector()一个把shallowEqual预置为默认相等性比较函数的便捷封装适合选择器返回对象/数组的常见场景import { useSelector, shallowEqual } from react-redux export function useShallowEqualSelector(selector) { return useSelector(selector, shallowEqual) }使用 Hooks 时的额外考量在决定是否使用 Hooks 时有一些架构上的权衡值得考虑例如 Hooks 与 HOC 在组件复用、关注点分离、代码组织上的取舍。总体而言对于新代码官方推荐默认采用 Hooks API对于既有使用connect()的代码可以逐步迁移因为两者可以共存于同一组件树中。与connect()的对比总结下表总结了useSelector()与connect()的mapState在关键行为上的差异便于快速对照对比维度useSelector()connect()的 mapState默认相等性比较严格引用比较结果对象的浅相等比较返回值类型任意值推荐单值通常为对象ownProps参数不提供可用闭包/柯里化作为第二个参数提供返回新对象默认每次都触发重渲染只要字段值不变就不重渲染订阅行为每次调用创建独立订阅每个已连接组件一个订阅阻止父级引起的重渲染不阻止可配React.memo有 props 比较优化深入阅读当前版本的 Hooks 文档docs/api/hooks.md其中还包含 v8.1.0 起新增的开发模式检查选择器结果稳定性检查stabilityCheck与恒等函数检查identityFunctionCheck说明Hooks 完整实现源码src/hooks/useSelector.ts、src/hooks/useDispatch.ts、src/hooks/useStore.ts、src/hooks/useReduxContext.tsProvider 与 Context 实现src/components/Provider.tsx、src/components/Context.ts订阅机制与浅相等实现src/utils/Subscription.ts、src/utils/shallowEqual.tsHooks 测试用例test/hooks/useSelector.spec.tsx、test/hooks/useDispatch.spec.tsx、test/hooks/useReduxContext.spec.tsx、test/hooks/hooks.withTypes.test.tsx可用于验证上述行为的真实表现。赞分享前端【免费下载链接】react-reduxOfficial React bindings for Redux项目地址https://gitcode.com/gh_mirrors/re/react-redux点击查看免费下载相关推荐React Redux Hooks 完全指南useSelector、useDispatch、useStore 与自定义 Context 实战React Redux Hooks 完全指南useSelector、useDispatch、useStore 与自定义 Context 实战 React Re前端React-Redux Hooks革命useSelector和useDispatch最佳实践React Redux Hooks革命useSelector和useDispatch最佳实践 React Redux Hooks彻底改变了我们在React应用前端Redux 与 React 集成实战useSelector、useDispatch 与 Provider 的完整工作流Redux 与 React 集成实战useSelector、useDispatch 与 Provider 的完整工作流 本文基于 Redux 官方仓库 Fun前端上一篇Foundations-of-LLMs数据文化价值观与行为规范下一篇Windows 11 精简完整指南用 tiny11builder 把安装镜像缩小近半创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →