为何Zustand选择器会触发「更新深度超出最大值」错误?
问题场景
Zustand Store 代码
interface PropertyStore { cache: Record<PropertyData["id"], PropertyData>; fetchProperty: (propertyId: PropertyData["id"]) => StoreResponse<undefined>; searchProperties: (filter: PropertySchemaFilter) => StoreResponse<undefined>; createProperty: (newPropertyData: CreatePropertyData) => StoreResponse<undefined>; updateProperty: (propertyId: PropertyData['id'], updatedProperty: UpdatePropertyData) => StoreResponse<undefined>; } export const usePropertyStore = create<PropertyStore>((set, get) => { const cacheProperties = (properties: PropertyData[]) => { const { cache } = get(); const cacheCopy = { ...cache }; properties.forEach((property) => { cacheCopy[property.id] = property; }); set({ cache: cacheCopy }); }; const newPropertyHandler = (newProperty: PropertyData) => { console.log(`event -> [${WsEvent.NewProperty}] ${newProperty.address} `); cacheProperties([newProperty]); } const updatedPropertyHandler = (updatedProperty: PropertyData) => { console.log(`event -> [${WsEvent.UpdatedProperty}] ${updatedProperty.address} `); cacheProperties([updatedProperty]); } propertiesSubscribe(newPropertyHandler, updatedPropertyHandler); return { cache: {}, fetchProperty: async (propertyId: PropertyData["id"]) => { const { data: property, error } = await getProperty(propertyId); if (error) { return { error }; } if (property) { cacheProperties([property]); } return { data: undefined }; }, searchProperties: async (filter) => { console.log(`propertyStore -> searchProperties ${JSON.stringify(filter)}`) const { error, data } = await getProperties(filter); if (error) { return { error }; } cacheProperties(data); return { data: undefined }; }, createProperty: async (newPropertyData) => { const { error } = await createProperty(newPropertyData); return error ? { error } : { data: undefined }; }, updateProperty: async (propertyId, updatedProperty) => { const { error } = await updateProperty(propertyId, updatedProperty); return error ? { error } : { data: undefined }; }, }; })
组件数据获取与Effect代码
const properties = usePropertyStore((store) => // <- line 31 Object.values(store.cache).filter((property) => propertyCompliesFilter(property, propertiesFilter) // <- line 33 ) ); const searchProperties = usePropertyStore((store) => store.searchProperties); //searchProperties effect useEffect(() => { console.log(`App -> effect [searchProperties]`); setSearchingProperties(true); setSearchPropertiesError(undefined); searchProperties(propertiesFilter) .then(({ error }) => { if (error) { setSearchPropertiesError(error); } }) .finally(() => { setSearchingProperties(false); }); }, [propertiesFilter, searchProperties]);
渲染代码
{properties.map((property) => ( <Marker key={property.id} position={property.coordinates} icon={getIconForProperty(property)} > <Popup> {property.id}: {property.address} <button type="button" onClick={() => openViewPropertyModal(property.id)} > Open details </button> </Popup> </Marker> ))}
日志信息
警告:getSnapshot的结果应被缓存以避免无限循环
在App组件中(http://localhost:5173/src/App.tsx?t=1737126342124:33:25)
App -> effect [searchProperties]
propertyStore -> searchProperties {"deleted":false}
realtorMapService -> getProperties
App -> effect [searchProperties]
propertyStore -> searchProperties {"deleted":false}
realtorMapService -> getProperties
未捕获错误:更新深度超出最大值。当组件在componentWillUpdate或componentDidUpdate中重复调用setState时可能会发生这种情况。React限制嵌套更新的次数以防止无限循环。
问题原因分析
未缓存的派生数据触发重复渲染
获取properties时,直接通过usePropertyStore返回新数组(Object.values(store.cache).filter(...)),每次组件渲染都会生成新数组引用。Zustand无法识别派生数据是否真的变化,会频繁触发组件更新,进而导致Effect重复执行。Effect依赖项与状态更新的循环
- Effect依赖项包含
searchProperties,而该函数每次Store更新(比如cache变化)时都会生成新引用,导致Effect重新触发。 - Effect触发后调用
searchProperties,该方法会更新Store的cache,进而再次触发组件更新,形成无限循环。
- Effect依赖项包含
WebSocket订阅的额外更新
Store初始化时的propertiesSubscribe,若WebSocket推送的新/更新属性符合当前过滤器,会更新cache,进一步加剧循环。
解决方案
1. 缓存派生的properties数据
使用Zustand的shallow比较或useMemo缓存过滤后的数组,避免不必要的渲染:
// 方法一:使用shallow比较 import { shallow } from 'zustand/shallow'; const properties = usePropertyStore( (store) => Object.values(store.cache).filter((property) => propertyCompliesFilter(property, propertiesFilter) ), shallow // 浅比较数组元素,避免新数组引用触发更新 ); // 方法二:结合useMemo缓存 const cache = usePropertyStore((store) => store.cache); const properties = useMemo(() => { return Object.values(cache).filter((property) => propertyCompliesFilter(property, propertiesFilter) ); }, [cache, propertiesFilter]);
2. 稳定searchProperties函数引用
获取Store函数时,使用shallow或直接调用Store的持久化方法,避免每次Store更新生成新函数:
// 使用shallow保持函数引用稳定 const { searchProperties } = usePropertyStore( (store) => ({ searchProperties: store.searchProperties }), shallow ); // 或者直接获取Store的持久化函数引用 const searchProperties = usePropertyStore.getState().searchProperties;
3. 优化Effect逻辑
确保Effect只在propertiesFilter真正变化时触发,移除不必要的依赖项:
useEffect(() => { console.log(`App -> effect [searchProperties]`); setSearchingProperties(true); setSearchPropertiesError(undefined); const fetchData = async () => { const { error } = await searchProperties(propertiesFilter); if (error) { setSearchPropertiesError(error); } }; fetchData().finally(() => { setSearchingProperties(false); }); }, [propertiesFilter]); // 仅依赖propertiesFilter
内容的提问来源于stack exchange,提问作者timo

