You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的订阅比较逻辑上:

  1. 你当前的选择器函数每次执行都会返回一个包含currentPage、initCurrentPage和updateBlock的新对象,这个新对象的引用和之前的完全不同。
  2. Zustand默认使用引用比较判断是否触发组件更新,只要选择器返回的对象引用变化,就会通知组件重渲染。
  3. 当你更新blocksForCurrentPage时,整个store状态发生变化,选择器重新执行并返回新对象,即使currentPage本身没有修改,React也会认为依赖更新,触发App组件重渲染。

解决方案

使用Zustand提供的shallow比较函数优化订阅逻辑,让选择器仅在currentPage的引用变化时才触发组件更新:

  1. 导入shallow:
import { create, shallow } from 'zustand'
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 18:05:05