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

React+TypeScript用Zustand出现最大更新深度超限错误的原因

问题描述

我正在学习Zustand状态管理库,结合TypeScript文档实现了一个支持课程添加、删除、状态切换的课程列表应用。在CourseList组件中,尝试通过解构方式从store一次性获取courses、removeCourse、toggleCourse时(注释代码),控制台抛出“Uncaught Error: Maximum update depth exceeded”错误;但将状态和方法分开调用useCourseStore获取时,应用运行正常。想知道该错误的原因是什么?

相关代码

courseStore.ts

import { create } from 'zustand';

import { devtools, persist } from 'zustand/middleware'

type Course = {
  id: Number, 
  title: String,
  completed: boolean,
}

interface StoreState {
  courses: Course[],
  addCourse: (course: Course) => void,
  removeCourse: (courseId: Number) => void,
  toggleCourse: (courseId: Number) => void,
}
const useCourseStore = create<StoreState>()(
  devtools(
    persist(
      (set) => ({
        courses: [],
        addCourse: (course) => {
          set((state) => ({
            courses: [course, ...state.courses]
          }))
        },
        removeCourse: (courseId) => {
          set((state) => ({
            courses: state.courses.filter((course) => course.id !== courseId)
          }))
        }, 
        toggleCourse: (courseId) => {
          set((state) => ({
            courses: state.courses.map((course) => course.id === courseId
              ? { ...course, completed: !course.completed }
              : course
            )
          }))
        }
      }),
      { name: 'courseStore' },
    ),
  ),
)

export default useCourseStore;

courseList.tsx

import React from 'react'
import useCourseStore from '../app/courseStore'

const CourseList = () => {
  // const {courses, removeCourse, toggleCourse} = useCourseStore(
  //     (state) => ({
  //         courses: state.courses,
  //         removeCourse: state.removeCourse,
  //         toggleCourse: state.toggleCourse, 
  //     })
  // )
  const courses = useCourseStore(state => state.courses)
  const removeCourse = useCourseStore((state) => state.removeCourse)
  const toggleCourse = useCourseStore((state) => state.toggleCourse)  
  
  return (
    <>
      <ul>
        {courses.map((course, i) => {
          return (
            <React.Fragment key={i}>
              <li
                className={`course-item`}
                style={{
                  backgroundColor: course.completed ? "#00FF0044" : "white"
                }}
              >
                <span className="course-item-col-1">
                  <input 
                    checked={course.completed}
                    type="checkbox"
                    onChange={() => {
                      toggleCourse(course.id)
                    }}
                  />
                </span>
                <span>{course?.title}</span>
                <button 
                  onClick={() => {
                    removeCourse(course.id)
                  }}
                  className="delete-btn"
                >
                  Delete
                </button>
              </li>
            </React.Fragment>
          )
        })}
      </ul>
    </>
  )
}

export default CourseList
错误原因分析

核心问题是每次组件渲染时,你传递给useCourseStore的选择器函数都会返回一个全新的对象引用。

Zustand的选择器默认通过引用对比判断状态是否变化:如果前后两次选择器返回值的引用不同,就会触发组件重新渲染。当你在选择器里返回{courses: state.courses, removeCourse: state.removeCourse, toggleCourse: state.toggleCourse}时,每次组件渲染都会创建一个新对象,Zustand会误判状态发生了变化,触发组件重新渲染;而重新渲染又会再次执行选择器创建新对象,形成无限循环,最终抛出“Maximum update depth exceeded”错误。

而分开调用useCourseStore获取单个值时,每个选择器返回的都是稳定引用:

  • courses是数组,只有当数组内容(元素或长度)变化时才会触发更新
  • removeCourse和toggleCourse是Zustand store中定义的方法,Zustand会保证这些方法的引用在store生命周期内始终稳定,不会触发不必要的渲染。

另外,你提到的useEffect是多余的——Zustand的useCourseStore本身就是React Hook,会自动订阅状态变化并触发组件更新,无需额外用useEffect包裹。

解决方案

如果想保持解构的写法,有两种可行方案:

方案1:使用shallow比较器

引入Zustand提供的shallow比较器,让选择器对比对象的属性值而非引用:

import { shallow } from 'zustand/shallow'

const CourseList = () => {
  const {courses, removeCourse, toggleCourse} = useCourseStore(
    (state) => ({
      courses: state.courses,
      removeCourse: state.removeCourse,
      toggleCourse: state.toggleCourse, 
    }),
    shallow // 添加shallow比较器
  )

  // ... 其余组件代码
}

方案2:拆分多个useCourseStore调用

就是你当前正常运行的写法,每个useCourseStore调用只获取单个状态或方法,确保返回的引用稳定:

const courses = useCourseStore(state => state.courses)
const removeCourse = useCourseStore((state) => state.removeCourse)
const toggleCourse = useCourseStore((state) => state.toggleCourse)  

内容的提问来源于stack exchange,提问作者kb456

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 08:47:37