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

