Redux Toolkit单数据变更触发列表全量重渲染优化方案
Redux Toolkit 列表单项更新触发全量重渲染优化方案
1. 替换为标准化状态结构,从数据源层减少引用变更
你当前写的fruits: [apple:4,pear:3,banana:5]不是合法的JavaScript数组结构,高性能场景下应该用规范化(Normalized)结构存储列表数据,直接用Redux Toolkit内置的createEntityAdapter生成状态操作逻辑即可,它默认会把数据存储为{ ids: [], entities: {} }的形态:单条数据更新时只会修改对应entities[id]的引用,不会改动整个entities对象,也不会改动ids数组(增删、排序场景除外),从根源上避免全量引用变更触发的重渲染。
基础配置示例:
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' // 假设你的getFruitsQuery、getFruitQuery、FruitMutation从api切片文件导入 import { getFruitsQuery, getFruitQuery, FruitMutation } from './fruitApi' const fruitsAdapter = createEntityAdapter() const initialState = fruitsAdapter.getInitialState() const fruitsSlice = createSlice({ name: 'fruits', initialState, reducers: {}, extraReducers: (builder) => { // 全量拉取水果列表时,批量写入实体 builder.addMatcher(getFruitsQuery.matchFulfilled, (state, { payload }) => { fruitsAdapter.setAll(state, payload) }) // 单条查询、变更操作返回时,只更新对应id的实体 builder.addMatcher( (action) => [getFruitQuery.fulfilled, FruitMutation.fulfilled].includes(action.type), (state, { payload }) => { fruitsAdapter.upsertOne(state, payload) } ) } }) // 导出内置记忆化选择器 export const { selectIds: selectFruitIds, selectById: selectFruitById } = fruitsAdapter.getSelectors(state => state.fruits)
2. 拆分组件数据订阅逻辑,缩小重渲染范围
不要在父列表组件中直接订阅完整的水果对象数组,单条数据更新时全量数组的引用会发生变化,导致父组件重渲染后连带所有子项重渲染。
正确的订阅逻辑是父组件只订阅id列表,子项单独订阅自身数据:
// 父列表组件 const FruitList = () => { // 仅订阅水果id数组,只有增删、排序操作才会触发该值变更 const fruitIds = useSelector(selectFruitIds) return ( <div className="fruit-list"> {fruitIds.map(id => <FruitItem key={id} fruitId={id} />)} </div> ) }
子项组件用React.memo包裹,内部仅订阅当前id对应的水果数据,只有自身数据变更时才会触发重渲染:
import { memo } from 'react' // 单条水果子组件 const FruitItem = memo(({ fruitId }) => { // 仅订阅当前id对应的水果数据,其他水果更新不会触发当前组件重渲染 const fruit = useSelector(state => selectFruitById(state, fruitId)) return ( <div className="fruit-item"> <span>{fruit.name}</span> <span>库存:{fruit.count}</span> </div> ) })
3. 配置精确的RTK Query缓存失效规则
如果你的FruitMutation触发后全量失效了水果列表缓存,会触发全量列表重新请求,也会导致全量重渲染。需要给接口配置精确的标签,单条变更时仅失效对应id的缓存即可:
// api切片配置片段 export const fruitApi = createApi({ reducerPath: 'fruitApi', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), tagTypes: ['Fruit'], endpoints: (builder) => ({ getFruits: builder.query({ query: () => '/fruits', providesTags: (result) => [ 'Fruit', ...(result || []).map(fruit => ({ type: 'Fruit', id: fruit.id })) ] }), getFruit: builder.query({ query: (id) => `/fruits/${id}`, providesTags: (_, __, id) => [{ type: 'Fruit', id }] }), fruitMutation: builder.mutation({ query: (data) => ({ url: `/fruits/${data.id}`, method: 'PUT', body: data }), // 仅失效当前操作id对应的缓存,不触发全量列表重拉 invalidatesTags: (_, __, arg) => [{ type: 'Fruit', id: arg.id }] }) }) })
注意:如果需要在选择器中做数据转换、派生计算,必须用
createSelector生成记忆化选择器,避免每次选择器执行返回新引用,触发不必要的重渲染。
内容的提问来源于stack exchange,提问作者Snick
相关产品推荐
相关产品推荐

