Zustand仅更新部分状态却触发整个React应用重绘问题求助
问题:Zustand状态管理下React应用不必要的重渲染
我用Zustand做状态管理写了个React测试应用,仅展示输入框,但每次在输入框输入时整个应用都会刷新。我确定是store实现存在问题,但无法定位具体位置。
控制台中in use event仅打印一次,符合预期,但Redraw app会在每次输入框按键时打印。明明更新输入框值时仅修改了blocksForCurrentPage数组中的元素,组件渲染仅依赖currentPage状态,并非blocksForCurrentPage,为何整个App仍会更新?
代码实现
主应用代码
import React, { useEffect } from 'react' import { create } from 'zustand' import { v4 as uuidv4 } from 'uuid'; const store = create((set, get) => ({ currentPage: { blocks: [] }, blocksForCurrentPage: [], findBlockById: blockId => get().blocksForCurrentPage.find(block => block.blockId === blockId), _addDummyBlockAtEndOfPage: _ => { const newDummyBlock = ({blockId: uuidv4(), content:'', type:undefined, isDummy: true}) set(state => ({ blocksForCurrentPage: [...state.blocksForCurrentPage, newDummyBlock] })) set(state => ({ currentPage: { ...state.currentPage, blocks: [...state.currentPage.blocks, newDummyBlock.blockId] } })) }, initCurrentPage: _ => { const blocks = get().currentPage?.blocks const lastBlockId = blocks.length > 0 ? blocks[blocks.length - 1] : undefined if (lastBlockId === undefined || !get().findBlockById(lastBlockId)?.isDummy) get()._addDummyBlockAtEndOfPage() }, updateBlock: ({ blockId, type, content }) => { //update the block content const index = get().blocksForCurrentPage.findIndex(block => block.blockId === blockId) if (index < 0) return set(state => { const block = state.blocksForCurrentPage[index] const newArray = [...state.blocksForCurrentPage] newArray.splice(index, 1, { ...block, type, content, isDummy: false }) return {blocksForCurrentPage: newArray} }) }, })) function App () { const { currentPage, initCurrentPage, updateBlock } = store((state) => ({ currentPage: state.currentPage, initCurrentPage: state.initCurrentPage, updateBlock: state.updateBlock, })) console.log('Redraw app') useEffect(() => { console.log('in use event') initCurrentPage() }, [currentPage]) const onChange = (t, blockId) => { updateBlock({ blockId, content: t.target?.value }) } return <> {currentPage?.blocks?.map((blockId) => <input key={blockId} onChange={t => onChange(t, blockId)}/>)} </> } export default App;
package.json
{ "name": "webclient", "version": "0.1.0", "private": true, "dependencies": { "immer": "^9.0.19", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", "uuid": "^9.0.0", "zustand": "^4.3.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
问题原因
核心问题出在Zustand的订阅比较逻辑上:
- 你当前的选择器函数每次执行都会返回一个包含
currentPage、initCurrentPage和updateBlock的新对象,这个新对象的引用和之前的完全不同。 - Zustand默认使用引用比较判断是否触发组件更新,只要选择器返回的对象引用变化,就会通知组件重渲染。
- 当你更新
blocksForCurrentPage时,整个store状态发生变化,选择器重新执行并返回新对象,即使currentPage本身没有修改,React也会认为依赖更新,触发App组件重渲染。
解决方案
使用Zustand提供的shallow比较函数优化订阅逻辑,让选择器仅在currentPage的引用变化时才触发组件更新:
- 导入
shallow:
import { create, shallow } from 'zustand'
- 修改App组件中的store订阅,添加
shallow作为第二个参数:
const { currentPage, initCurrentPage, updateBlock } = store( (state) => ({ currentPage: state.currentPage, initCurrentPage: state.initCurrentPage, updateBlock: state.updateBlock, }), shallow )
shallow会对选择器返回的对象进行浅比较,只有当对象内的属性(这里是currentPage、initCurrentPage、updateBlock)的引用发生变化时,才会触发组件重渲染。由于Zustand的方法(initCurrentPage、updateBlock)是稳定的,不会随状态变化而重新创建,而currentPage在输入时并未修改,因此组件不会因为blocksForCurrentPage的更新而触发重渲染。
内容的提问来源于stack exchange,提问作者user2097439
相关产品推荐
相关产品推荐

