首页
/ Redux Essentials 实战:选择器记忆化、数据规范化与 createListenerMiddleware 响应式逻辑

Redux Essentials 实战:选择器记忆化、数据规范化与 createListenerMiddleware 响应式逻辑

2026-09-04 12:41:20作者:咎竹峻Karen

本篇围绕 Redux Essentials 官方教程 Part 6 展开:在 React + Redux 应用中,如何用 createSelector 记忆化选择器消除无意义的组件重渲染,如何用 Redux Toolkit 的 createEntityAdapter 把数组存储重构为 {ids, entities} 规范化结构,以及如何用 createListenerMiddleware 编写响应于 dispatch 的副作用逻辑。读完后你将掌握一套完整的生产级性能优化与数据管理方案,并能结合本仓库源码理解其中间件管线与订阅机制的底层依据。

前置背景:为什么会出现这些问题

Part 6 建立在 Part 5 异步数据获取的基础上(参见 part-5-async-logic.md)。教程围绕一个带 postsusersauthnotifications 四个 slice 的示例应用推进,先补齐用户页面和通知功能,再逐一暴露并修复两类典型缺陷:

  1. 选择器内部 filter() 每次返回新数组引用,导致组件在每次 action 后都重渲染;
  2. 列表以数组方式存储,父组件一渲染子项全部跟着渲染,且按 ID 查找只能线性遍历数组。

此外还有一个贯穿性的认识:本仓库核心 dispatch 实现(见 src/createStore.ts)中,reducer 执行完 currentState = currentReducer(currentState, action) 后,是否真正产生状态变化完全由 reducer 的返回值决定——"本次 dispatch 不需要任何状态变更"是 reducer 合法的选择。这正是后文"dispatch 了 action 但状态可以不变"这一结论的底层依据。

补齐应用功能:用户页面与服务端登录

添加用户列表与用户详情页

<UsersList> 遵循标准模式:用 useSelector 读取数据并 map 渲染:

import { Link } from 'react-router-dom'
import { useAppSelector } from '@/app/hooks'
import { selectAllUsers } from './usersSlice'

export const UsersList = () => {
  const users = useAppSelector(selectAllUsers)

  const renderedUsers = users.map(user => (
    <li key={user.id}>
      <Link to={`/users/${user.id}`}>{user.name}</Link>
    </li>
  ))

  return (
    <section>
      <h2>Users</h2>
      <ul>{renderedUsers}</ul>
    </section>
  )
}

<UserPage> 从路由参数取 userId,渲染该用户的全部帖子。注意这里埋下了本节的"事故现场"——selectPostsByUser 在 selector 内部调用了 filter()

export const selectPostsByUser = (state: RootState, userId: string) => {
  const allPosts = selectAllPosts(state)
  // ❌ 危险模式!后文会解释原因
  return allPosts.filter(post => post.user === userId)
}

路由与导航栏随后加上 /users/users/:userId 两个入口(App.tsx 中注册 <Route>Navbar.tsx 中加 <Link to="/users">Users</Link>)。

登录逻辑改为异步 thunk

原本 <LoginPage>authSlice 只是 dispatch 客户端 action 记录用户名,实际需要向服务器发送登录请求。改造方式与 posts、users 一致:用 createAppAsyncThunk 定义 login/logout,slice 中删除对应 reducer,改在 extraReducers 中处理 thunk 生命周期:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '@/api/client'
import type { RootState } from '@/app/store'
import { createAppAsyncThunk } from '@/app/withTypes'

interface AuthState {
  username: string | null
}

export const login = createAppAsyncThunk(
  'auth/login',
  async (username: string) => {
    await client.post('/fakeApi/login', { username })
    return username
  }
)

export const logout = createAppAsyncThunk('auth/logout', async () => {
  await client.post('/fakeApi/logout', {})
})

const initialState: AuthState = { username: null }

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {},
  extraReducers: builder => {
    builder
      .addCase(login.fulfilled, (state, action) => {
        state.username = action.payload
      })
      .addCase(logout.fulfilled, state => {
        state.username = null
      })
  }
})

export default authSlice.reducer

<Navbar><LoginPage> 改为 dispatch 新 thunk;postsSlice 原本监听 userLoggedOut action creator,也相应改为监听 logout.fulfilled,在登出时清空帖子列表(return initialState)。

通知功能:从拉取数据到追踪已读状态

通知 Slice 与 thunk 参数

新建 notificationsSlice,异步 thunk fetchNotifications 从假 API 拉取"比最新一条更新"的通知——为此它需要读取当前 state 里最新通知的时间戳。这引出了 createAsyncThunk第二个参数 thunkAPI

export const fetchNotifications = createAppAsyncThunk(
  'notifications/fetchNotifications',
  async (_unused, thunkApi) => {
    const allNotifications = selectAllNotifications(thunkApi.getState())
    const [latestNotification] = allNotifications
    const latestTimestamp = latestNotification ? latestNotification.date : ''
    const response = await client.get<ServerNotification[]>(
      `/fakeApi/notifications?since=${latestTimestamp}`
    )
    return response.data
  }
)

thunkAPI 对象包含五个关键成员:

成员 用途
dispatch / getState store 真实的方法,可在 thunk 中再 dispatch 或读取最新 state
extra 创建 store 时传给 thunk 中间件的"extra argument",通常是 API 封装函数
requestId 本次 thunk 调用的唯一随机 ID,用于追踪单个请求的状态
signal AbortController.signal,可取消进行中的请求
rejectWithValue 自定义 rejected action 内容的工具函数

注意细节:createAsyncThunk 的 payload creator 第一个参数是 dispatch 时传入的参数,只支持一个。若不传,该参数就是 undefined——所以必须给第一个形参占位(此处命名 _unused)才能访问到第二个参数 thunkApi。如果手写 thunk(不用 createAsyncThunk),则 dispatchgetState 是分开的独立参数传入。

slice 侧在 fetchNotifications.fulfilled 时把新通知 push 进 state 并按日期倒序排序。教程特别提醒:array.sort() 会原地修改数组,这里之所以安全,是因为 createSlice 内部使用 Immer 对 draft 状态进行写操作。

客户端元数据:read / isNew

"新消息/未读"只是客户端概念,因此定义扩展类型并在接收服务器数据时补默认值:

export interface ServerNotification {
  id: string
  date: string
  message: string
  user: string
}

export interface ClientNotification extends ServerNotification {
  read: boolean
  isNew: boolean
}
  • allNotificationsRead reducer:把所有通知标记为已读;
  • fetchNotifications.fulfilled:新通知统一带 read: false, isNew: true 元数据;已存在的通知按"已读则不再是新"的语义更新 isNew
  • 新增选择器 selectUnreadNotificationsCount 统计未读数量,供导航栏角标使用。

用 useLayoutEffect 标记已读:一个"两次 dispatch"的教学案例

<NotificationsList> 每次渲染都在 useLayoutEffect 中 dispatch allNotificationsRead()(用 useLayoutEffect 是为了避免数据闪烁):

useLayoutEffect(() => {
  dispatch(allNotificationsRead())
})

这里出现了一个看似奇怪的现象:一次操作会 dispatch 两次 allNotificationsRead。原因链是:组件挂载 → 第一次 dispatch → reducer 不可变地更新条目,state.notifications 产生新数组引用 → useSelector 检测到新引用 → 组件重渲染 → useLayoutEffect 再跑一次、再次 dispatch → 这次 reducer 没有任何数据变化,slice 状态与根状态保持引用不变,组件不再重渲染

这正是 Redux 的一个核心语义:dispatch 一个 action 完全可以不引起任何状态变化,是否更新状态是 reducer 的自主决定。结合本仓库 src/createStore.ts 的实现可以看到:dispatch 只是把 currentReducer 的返回值赋给 currentState 并通知订阅者,"是否变了"取决于 reducer 是否返回了新引用。教程结论是:可以拆分"挂载时 dispatch 一次 + 数组长度变化时再 dispatch"来避免第二次调用,但"反正无副作用就不必处理"也是合理取舍。

导航栏则用 selectUnreadNotificationsCount 渲染未读角标 <span className="badge">{numUnreadNotifications}</span>

改进渲染性能:记忆化选择器

用 React Profiler 定位问题

<UserPage> 打开时点击 "Refresh Notifications",React DevTools Profiler 会显示:<Navbar> 重渲染合理(要更新未读角标),<UserPage> 也重渲染了——它并不读任何通知状态:

React DevTools Profiler 捕获:点击刷新通知时 UserPage 不必要地重渲染

排查 selectPostsByUser 就能找到根因:filter() 每次调用都产生一个新数组引用。而 useSelector 的语义是:每次 action 后重跑 selector,若返回值与上次引用不等就强制组件重渲染。于是这个选择器导致组件在每个 action 之后都重渲染,即使帖子数据毫无变化。

React-Redux 在开发模式会对此发出警告:

Selector unknown returned a different result when called with the same parameters.
This can lead to unnecessary rerenders.
Selectors that return a new reference (such as an object or an array) should be memoized:
    at UserPage (http://localhost:5173/src/features/users/UserPage.tsx)

这个示例 app 里影响不大,但在真实大型应用中,"不该渲染的组件额外重渲染"是常见且严重的性能问题。

用 createSelector 记忆化

"记忆化(memoization)"的思路是:缓存上一组输入和计算结果,输入没变就直接返回旧结果。Reselect 库为此设计,Redux Toolkit 直接 re-export 了其 createSelector。重写后的 selectPostsByUser

import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit'

export const selectAllPosts = (state: RootState) => state.posts.posts

export const selectPostsByUser = createSelector(
  // 一个或多个"输入选择器"
  [
    selectAllPosts, // 读取根 state 中某个值的已有选择器
    (state: RootState, userId: string) => userId // 提取调用参数
  ],
  // 输出函数:输入值任一变化才重新执行
  (posts, userId) => posts.filter(post => post.user === userId)
)

工作机制:调用 selectPostsByUser(state, userId) 时,createSelector 把全部参数传给每个输入选择器,输入选择器的返回值作为输出函数的实参。执行示例:

const state1 = getState()
// 第一次调用,输出函数执行
selectPostsByUser(state1, 'user1')
// 参数未变,输出函数不执行
selectPostsByUser(state1, 'user1')
// userId 变了,输出函数执行
selectPostsByUser(state1, 'user2')

dispatch(fetchUsers()) // 只改了 users,posts 没变
const state2 = getState()
// posts 和 userId 都没变,输出函数不执行
selectPostsByUser(state2, 'user2')

dispatch(addNewPost()) // posts 变了
const state3 = getState()
// posts 变了,输出函数执行
selectPostsByUser(state3, 'user2')

记忆化之后重跑 Profiler,<UserPage> 不再重渲染。

并非所有选择器都需要记忆化

判断标准:只有当选择器返回新建的对象/数组引用,或计算逻辑"昂贵"时才需要记忆化。以 selectUnreadNotificationsCount 为例——它内部虽有 .filter(),但返回的是数字(unreadNotifications.length),引用不会变化,作为普通函数就足够安全;当然把它也改成记忆化选择器可以省掉每次重新 filter 的开销,只是"必要性"远低于返回新引用的场景。

补充一个与本仓库文档一致的细节(见 docs/usage/deriving-data-selectors.md):createSelector 默认每个选择器实例只缓存最近一组参数。若同一个选择器要在多个位置以不同参数复用(如不同 postId),每次输入变化都会重跑输出函数——这时应考虑"选择器工厂"模式,或改用 createSelectorFactory

优化列表渲染:三种选项与规范化

列表全量重渲染的成因

<PostsList> 中点击某条帖子的反应按钮时,Profiler 显示所有 <PostExcerpt> 都重渲染了:

React DevTools Profiler 捕获:点击单条帖子的反应按钮导致全部 PostExcerpt 重渲染

原因是 React 的默认行为:父组件渲染时,会递归渲染其所有子组件。不可变更新一条帖子意味着 posts 数组引用变了,<PostsList> 必须重渲染,随后 React 继续向下渲染了每一个 <PostExcerpt>

可选优化路径有三条:

  1. React.memo:把 <PostExcerpt> 包进 React.memo(),仅在 props 真正变化时重渲染——最简单有效;
  2. 改为选择 ID 列表<PostsList> 只选 ID 数组,子组件收 postId 自己 useSelector 取数据;配合 useSelector(selectPostIds, shallowEqual) 按内容比较,可跳过 ID 集合未变的渲染。前提是 slice 侧保持数组有序;
  3. reducer 维护独立 ID 数组 + 实体查找表:ID 数组只在增删时变化,<PostsList> 仅因此重渲染。

第三条正是 createEntityAdapter 要解决的问题。

规范化状态结构

"规范化(normalization)"指:

  • 每份数据在 state 中只有一份拷贝,无重复;
  • 数据存入以 ID 为键的查找表(普通 JS 对象即可充当 map/dictionary);
  • 通常另有一个该类型全部 ID 的数组。
{
  users: {
    ids: ["user1", "user2", "user3"],
    entities: {
      "user1": {id: "user1", firstName, lastName},
      "user2": {id: "user2", firstName, lastName},
      "user3": {id: "user3", firstName, lastName},
    }
  }
}

按 ID 取项从"遍历整数组 find()"变成 O(1) 的直接索引:

const userObject = state.users.entities[userId]

更多背景参见 Normalizing State Shape

createEntityAdapter 的核心能力

createEntityAdapter 把一组条目装进 { ids: [], entities: {} } 结构,并生成配套工具。收益:

  • 免去手写规范化维护代码;
  • 内置 reducer 函数覆盖"批量添加/更新一项/删除多项"等常见用例;
  • 可选 sortComparer(与 Array.sort() 相同签名)让 ID 数组保持有序,且仅在增删或排序变化时更新数组。

返回的 adapter 对象含三块能力:

成员 说明
CRUD reducer 函数 setAlladdOneaddManyupsertOneupsertManyupdateOneremoveOneremoveMany 等;既可作 action 的 case reducer,也可作为"mutating"辅助函数在其他 reducer 内调用
getSelectors(stateSliceSelector) 传入"从根 state 取本 slice"的选择器,生成 selectAllselectByIdselectIds
getInitialState(extraFields?) 生成空 {ids: [], entities: {}},可传入额外字段(如 loading 状态)合并进来

将 postsSlice 规范化

import {
  createEntityAdapter,
  EntityState
} from '@reduxjs/toolkit'

interface PostsState extends EntityState<Post, string> {
  status: 'idle' | 'pending' | 'succeeded' | 'rejected'
  error: string | null
}

const postsAdapter = createEntityAdapter<Post>({
  // 按日期倒序
  sortComparer: (a, b) => b.date.localeCompare(a.date)
})

const initialState: PostsState = postsAdapter.getInitialState({
  status: 'idle',
  error: null
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postUpdated(state, action: PayloadAction<PostUpdate>) {
      const { id, title, content } = action.payload
      const existingPost = state.entities[id]
      if (existingPost) {
        existingPost.title = title
        existingPost.content = content
      }
    },
    reactionAdded(
      state,
      action: PayloadAction<{ postId: string; reaction: ReactionName }>
    ) {
      const { postId, reaction } = action.payload
      const existingPost = state.entities[postId]
      if (existingPost) {
        existingPost.reactions[reaction]++
      }
    }
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        postsAdapter.setAll(state, action.payload)
      })
      .addCase(addNewPost.fulfilled, postsAdapter.addOne)
  }
})

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

export default postsSlice.reducer

// 用 getSelectors 生成定制选择器,重命名以匹配旧名
export const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds
} = postsAdapter.getSelectors((state: RootState) => state.posts)

export const selectPostsByUser = createSelector(
  [selectAllPosts, (state: RootState, userId: string) => userId],
  (posts, userId) => posts.filter(post => post.user === userId)
)

逐点拆解:

  • PostsStateposts: Post[] 变为继承 EntityState<Post, string>(即 {ids: string[], entities: Record<string, Post>}),保留 status/error
  • 实体进入 state.entities 查找表后,reactionAddedpostUpdated 直接以 state.entities[postId] 定位,不再遍历数组;
  • fetchPosts.fulfilledpostsAdapter.setAll(state, action.payload) 是"在 createSlice reducer 内以 mutating 方式调用 adapter 方法"的典型用法;
  • addNewPost.fulfilled 则把 postsAdapter.addOne 直接作为 case reducer 传入;
  • 手写选择器由 getSelectors 的生成版本替换——因为选择器以根 state 调用,必须传入 (state) => state.posts 告知 slice 位置;生成函数固定叫 selectAll/selectById,用解构重命名对齐旧导出名;
  • 进阶:postUpdated 可进一步简化为 postsAdapter.updateOne(state, { id, changes: { title, content } });而 reactionAdded 不能updateOne,因为它不是整字段替换,而是对嵌套计数的自增,直接查表加"mutating"更新更合适。

优化后的列表组件

<PostsList> 只选有序 ID 数组,<PostExcerpt>postId 自行取实体:

interface PostExcerptProps {
  postId: string
}

function PostExcerpt({ postId }: PostExcerptProps) {
  const post = useAppSelector(state => selectPostById(state, postId))
  // omit rendering logic
}

export const PostsList = () => {
  const dispatch = useAppDispatch()
  const orderedPostIds = useAppSelector(selectPostIds)
  // omit other selections and effects

  if (postStatus === 'pending') {
    content = <Spinner text="Loading..." />
  } else if (postStatus === 'succeeded') {
    content = orderedPostIds.map(postId => (
      <PostExcerpt key={postId} postId={postId} />
    ))
  } else if (postStatus === 'rejected') {
    content = <div>{postsError}</div>
  }
  // omit other rendering
}

再点一次反应按钮并捕获 Profiler,这次只有被点击的那一个 <PostExcerpt> 重渲染:

React DevTools Profiler 捕获:规范化 + ID 列表后,仅单个 PostExcerpt 重渲染

users 与 notifications 的规范化

usersSlice 只需三处改动:建 adapter、initialState = usersAdapter.getInitialState()fetchUsers.fulfilled 直接用 usersAdapter.setAll 作 case reducer,选择器改由 getSelectors((state) => state.users) 生成。一个小坑:生成的 selectUserById 不接受可能为 nullcurrentUsernameselectCurrentUser 需要显式判空提前返回。

notificationsSlice 同理使用 notificationsAdapter(带 sortComparer 保持最新在前)。由于通知不再存于数组,需要全量遍历标记 isNew 时改用 Object.values(state.entities).forEach(...);而拉取落库逻辑则从手写 push+sort 换成 notificationsAdapter.upsertMany(state, notificationsWithMetadata)

编写响应式逻辑:createListenerMiddleware

为什么需要监听中间件

此前所有行为都是"命令式"的:用户操作 → 点击处理器或 useEffect 里 dispatch。但有时逻辑应该是对已发生的事做出反应(如 addNewPost.fulfilled 后弹 toast 提示)。多个 slice 监听同一个 action 只能用于"再更新一部分 state";异步或含副作用的逻辑不能放进 reducer——reducer 必须纯粹、无副作用。而中间件正是 Redux 中为副作用而设计的扩展点,thunk 中间件解决"现在就执行"的异步逻辑,监听中间件解决"当特定 action 被 dispatch 时再执行"的响应逻辑。

createListenerMiddleware 可类比 React 的 useEffect:区别在于它定义在 Redux 逻辑层而非组件里,且响应的是 dispatch 的 action 与状态更新,而不是渲染生命周期。

配置监听中间件

新建 app/listenerMiddleware.ts,按 createAsyncThunk 的做法用 .withTypes() 固化 store 类型:

import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
import type { RootState, AppDispatch } from './store'

export const listenerMiddleware = createListenerMiddleware()

export const startAppListening = listenerMiddleware.startListening.withTypes<
  RootState,
  AppDispatch
>()
export type AppStartListening = typeof startAppListening

export const addAppListener = addListener.withTypes<RootState, AppDispatch>()
export type AppAddListener = typeof addAppListener

createListenerMiddleware() 返回对象的三个字段:

  • listenerMiddleware.middleware:真正的 Redux 中间件实例,需加入 store;
  • listenerMiddleware.startListening:直接向中间件添加 listener 条目;
  • listenerMiddleware.addListener:一个 action creator,可在任何能拿到 dispatch 的地方 dispatch 以动态添加 listener,无需导入 middleware 对象本身。

注册到 store:

import { configureStore } from '@reduxjs/toolkit'
import { listenerMiddleware } from './listenerMiddleware'

export const store = configureStore({
  reducer: {
    auth: authReducer,
    posts: postsReducer,
    users: usersReducer,
    notifications: notificationsReducer
  },
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware().prepend(listenerMiddleware.middleware)
})

这里"顺序"很关键,可以从本仓库源码得到印证。中间件管线在 src/applyMiddleware.ts 中构建:

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

src/compose.tscompose从右向左组合:compose(f, g, h) 等价于 (...args) => f(g(h(...args)))。因此 middlewares 数组中越靠前的中间件越外层、最先拦截 dispatch,链路形如 m1 -> m2 -> m3 -> store.dispatch()。监听中间件需要最先拦截并处理部分 action,所以放在管线头部:getDefaultMiddleware() 返回的是数组,configureStore 在其上扩展了 .concat() 的对称方法 .prepend(),专门把新中间件插到数组开头。thunk 中间件由 configureStore 默认内置(开发模式还附带安全检查中间件),prepend 保留默认集的同时把 listener 排在最前。

监听新帖并弹出 toast

约定把 listener 定义在逻辑最相关的 slice 文件中,但在 slice 里不直接导入 startAppListening(保持导入链一致),而是导出一个接收它的函数:

import { AppStartListening } from '@/app/listenerMiddleware'

export const addPostsListeners = (startAppListening: AppStartListening) => {
  startAppListening({
    actionCreator: addNewPost.fulfilled,
    effect: async (action, listenerApi) => {
      const { toast } = await import('react-tiny-toast')

      const toastId = toast.show('New post added!', {
        variant: 'success',
        position: 'bottom-right',
        pause: true
      })

      await listenerApi.delay(5000)
      toast.remove(toastId)
    }
  })
}

然后在 app/listenerMiddleware.ts 末尾调用 addPostsListeners(startAppListening)——与 app/store.ts 从各 slice 导入 reducer 的方式同构。

添加 listener 时,匹配条件三选一:

选项 说明
actionCreator 某个 RTK action creator,如 addNewPost.fulfilled;该 action 被 dispatch 时运行 effect
matcher RTK matcher 函数,如 isAnyOf(reactionAdded, addNewPost.fulfilled);返回 true 时运行
predicate (action, currState, prevState) => boolean,可做任意检查,例如比较 currState.counter.value !== prevState.counter.value 检测状态变化

effect 回调形如异步 thunk:第一个参数是匹配到的 action,第二个是 listenerApi 对象——除了常规的 dispatchgetState,还提供 condition()(暂停直到其他 action 被 dispatch 或某状态值变化)、unsubscribe()/subscribe()(控制该 listener 条目的激活状态)、fork()(派生子任务)等,足以支撑复杂的异步工作流。

<App> 中还需渲染 react-tiny-toast<ToastContainer />。最终效果:新增帖子成功后,右下角弹出绿色 toast,5 秒后消失——整个过程中 React 组件侧没有新增任何业务逻辑,响应完全由 store 中的监听中间件在 action 被 dispatch 后触发。

本篇要点回顾

  • 记忆化选择器优化性能:Redux Toolkit re-export 的 createSelector 生成记忆化选择器;仅当输入选择器返回新值时重算输出,既跳过昂贵计算又保证同一结果引用。
  • 多种渲染优化模式可组合:避免在 useSelector 中返回新对象/数组引用;传记忆化选择器给 useSelector;用 shallowEqual 等替代比较函数;React.memo 包裹组件;列表父组件只读 ID 数组、子组件按 ID 取实体。
  • 规范化状态是推荐的存储方式:数据不重复、以 {ids: [], entities: {}} 查找表组织。
  • createEntityAdapter 承接规范化:sortComparer 保持 ID 有序;getInitialState 可合并 loading 状态;setAll/addMany/upsertOne/removeMany 等内置 reducer;getSelectors 生成 selectAll/selectById
  • createListenerMiddleware 运行响应式逻辑:需在 store 中以带类型的形式装配(用 prepend 置于管线头部);listener 通常定义在 slice 文件中;可按单 action、多 action 或自定义谓词匹配;effect 内可写任意同步/异步逻辑,listenerApi 提供 conditionforkunsubscribe 等编排工具。

后续章节将进入 RTK Query(见 part-7-rtk-query-basics.md),它是 Redux Toolkit 提供的数据获取与缓存 API,可完全省掉手写的获取逻辑;而 selector 的一般性写法与 createSelector 的进阶行为(缓存策略、嵌套记忆化等)可进一步参考 docs/usage/deriving-data-selectors.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384