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

为何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限制嵌套更新的次数以防止无限循环。


问题原因分析

  1. 未缓存的派生数据触发重复渲染
    获取properties时,直接通过usePropertyStore返回新数组(Object.values(store.cache).filter(...)),每次组件渲染都会生成新数组引用。Zustand无法识别派生数据是否真的变化,会频繁触发组件更新,进而导致Effect重复执行。

  2. Effect依赖项与状态更新的循环

    • Effect依赖项包含searchProperties,而该函数每次Store更新(比如cache变化)时都会生成新引用,导致Effect重新触发。
    • Effect触发后调用searchProperties,该方法会更新Store的cache,进而再次触发组件更新,形成无限循环。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 21:54:58