如何避免Zustand存储中数组映射组件的全量重渲染
优化Zustand数组状态下的React组件重渲染问题
现有代码实现
Zustand状态存储
const testData: BoxData[] = [ { id: "1", classId: 1, text: "Box 1" }, { id: "2", classId: 1, text: "Box 2" }, { id: "3", classId: 1, text: "Box 3" }, { id: "4", classId: 1, text: "Box 4" }, ]; const useStore = create<StoreState>((set) => ({ boxes: testBoxes, setBoxes: (boxes: BoxData[]) => set(() => ({ boxes })), })); export const useBoxes = () => useStore((state) => state.boxes); export const useSetBoxes = () => useStore((state) => state.setBoxes);
父组件BoxSection
const BoxSection = () => { const boxes = useBoxes(); return ( <div> {boxes.map((box) => ( <BoxItem key={box.id} box={box} /> ))} </div> ); };
子组件BoxItem
const BoxItem: React.FC<boxItemProps> = ({ box }) => { const setBoxes = useSetBoxes(); const boxes = useBoxes(); const onDeleteBox = (id: string) => { const newBoxes = boxes.filter((box) => box.id !== id); setBoxes(newBoxes); }; const onIncreaseClassId = (id: string) => { const newBoxes = boxes.map((box) => { if (box.id === id) { return { ...box, classId: box.classId + 1, }; } return box; }); setBoxes(newBoxes); }; return ( <div> <button onClick={() => onDeleteBox(box.id)}>Delete</button> <button onClick={() => onIncreaseClassId(box.id)}> Increase ClassID </button> </div> ); };
当前问题
修改单个box数据时,所有BoxItem实例都会触发重渲染。尝试过通过父组件传递useCallback包裹的处理函数,但由于依赖数组包含boxes会引发循环依赖,最终还是会导致全量重渲染。
实际场景需求
需处理包含200页的文档,每页最多20个box组件;不同UI区域仅关注box的特定属性(比如翻译侧边栏只关心text,另一侧边栏只渲染classId),需要精准控制重渲染范围,避免性能损耗。
解决方案
1. 重构Zustand状态结构与选择器
将数组转为以id为键的对象结构,同时内置单个数据的操作方法,让组件可以精准订阅所需数据:
// 重构状态存储 const useStore = create<StoreState>((set) => ({ boxes: Object.fromEntries(testData.map(box => [box.id, box])), // 转为 { [id: string]: BoxData } 结构 // 更新单个box属性的方法 updateBox: (id: string, updates: Partial<BoxData>) => set(state => ({ boxes: { ...state.boxes, [id]: { ...state.boxes[id], ...updates } } })), // 删除单个box的方法 deleteBox: (id: string) => set(state => { const newBoxes = { ...state.boxes }; delete newBoxes[id]; return { boxes: newBoxes }; }) })); // 订阅单个box的选择器 export const useBox = (id: string) => useStore(state => state.boxes[id]); // 订阅所有box的id列表(用于父组件遍历) export const useBoxIds = () => useStore(state => Object.keys(state.boxes)); // 获取操作方法 export const useUpdateBox = () => useStore(state => state.updateBox); export const useDeleteBox = () => useStore(state => state.deleteBox);
2. 优化BoxItem组件
组件仅订阅当前需要的单个box数据,操作无需依赖整个数组:
const BoxItem: React.FC<{ boxId: string }> = ({ boxId }) => { const box = useBox(boxId); const updateBox = useUpdateBox(); const deleteBox = useDeleteBox(); const onDeleteBox = () => { deleteBox(boxId); }; const onIncreaseClassId = () => { updateBox(boxId, { classId: box.classId + 1 }); }; return ( <div> <button onClick={onDeleteBox}>Delete</button> <button onClick={onIncreaseClassId}> Increase ClassID </button> </div> ); };
3. 优化父组件BoxSection
父组件仅订阅box的id列表,避免订阅整个box对象数组:
const BoxSection = () => { const boxIds = useBoxIds(); return ( <div> {boxIds.map((id) => ( <BoxItem key={id} boxId={id} /> ))} </div> ); };
4. 特定属性的精准订阅
针对不同UI区域的需求,编写精准的选择器,只返回所需属性:
// 翻译侧边栏专用:订阅所有box的id和text属性 export const useBoxTexts = () => useStore(state => Object.values(state.boxes).map(box => ({ id: box.id, text: box.text })) ); // 另一侧边栏专用:订阅所有box的id和classId属性 export const useBoxClassIds = () => useStore(state => Object.values(state.boxes).map(box => ({ id: box.id, classId: box.classId })) );
核心原理
- 状态扁平化:数组转对象后,组件可以精准订阅单个数据项,而非整个数组,从根源减少重渲染触发条件。
- Zustand选择器特性:Zustand会对选择器返回值做浅比较,只有当返回值变化时才触发组件重渲染,精准选择器能大幅降低不必要的更新。
- 内置操作方法:通过状态存储提供的
updateBox、deleteBox方法,组件无需获取整个数组即可完成操作,彻底消除循环依赖和全量重渲染问题。
内容的提问来源于stack exchange,提问作者GanGan Duh
相关产品推荐
相关产品推荐

