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

Zustand多异步函数共用loading状态引发无限重渲染问题

Zustand共享loading状态引发无限重渲染与永久加载问题

问题场景

使用Zustand 4.1.5的React应用中,进入FairDetails页面的LogDetails标签页时,两个共用同一loading状态的异步函数并发执行,导致:

  • 应用无限重渲染
  • index.js中的<ComponentSpinner />永久显示,但实际loading状态为false

相关代码

Zustand Store

const initialState = { 
  selectedFair: {}, 
  params: {}, 
  fairLogDetails: {}, 
  loading: false, 
  error: null 
}
    
const useFairStore = create((set, get) => ({
  ...initialState,   
  getFairs: async (params) => { 
    const endpoint = params?.role === 'admin' ? '/fair/admin' : '/fair/all' 
    set({ loading: true }) 
    try { 
      const { data } = await axios(`${endpoint}?page=${params?.page || 1}&limit=${params?.limit || 10}`) 
      set({ fairs: data?.data, totalFairs: data?.total, params, error: null }) 
    } catch (e) { 
      set({ error: e }) 
    } finally { 
      set({ loading: false }) 
    } 
  },
  getFairLogDetails: async (params, reqBody = {}) => { 
    set({ loading: true })
    try {
      const { fairId } = params
      const response = await axios.post(`/report/fair-statistics/${fairId}`, reqBody)
      set({
        fairLogDetails: response?.data,
        error: null
      })
    } catch (e) {
      set({ error: e })
      return Promise.reject({ 
        code: e?.response?.status, 
        message: e?.response?.data?.message 
      })
    } finally {
      set({ loading: false })
    }
  },
}))

FairDetails/index.js

const { fairId } = useParams()
const [selectedFair, getFair, loading, cleanStore] = useFairStore( 
  (state) => [state.selectedFair, state.getFair, state.loading, state.cleanStore], 
  shallow
)
    
useEffect(() => { 
  getFair(fairId)
  return () => cleanStore()
}, [fairId])

...
    
if (loading) return <ComponentSpinner />
    
return (
  // 包含LogDetails组件的内容
)

LogDetails.js

const [selectedFair, getFairLogDetails, fairLogDetails] = useFairStore( 
  (state) => [state.selectedFair, state.getFairLogDetails, state.fairLogDetails], 
  shallow 
)

useEffect(() => {
  getFairLogDetails({fairId: selectedFair?._id})
}, [selectedFair?._id])
 
return (
  // 日志详情内容
)

原因分析

  1. 共享loading状态的并发冲突:两个异步函数共用同一个loading字段,并发执行时,第一个函数的finally块把loading设为false,但第二个函数仍在执行,会立刻把loading重新设为true,导致loading状态在true和false之间快速切换,触发组件频繁重渲染。
  2. 组件渲染循环触发:index.js中当loading为true时返回Spinner,状态切换会导致Spinner和子组件反复卸载/挂载;子组件LogDetails挂载后又会触发getFairLogDetails,再次修改loading状态,形成循环。
  3. selectedFair引用变化触发重复请求:getFair更新selectedFair时,每次都会生成新的对象引用,即使内容没有变化,shallow比较会认为状态更新,触发LogDetails的useEffect重新执行,再次调用getFairLogDetails,加剧循环。

解决方案

1. 拆分独立的loading状态

为每个异步操作单独设置loading字段,避免互相干扰:

const initialState = { 
  selectedFair: {}, 
  params: {}, 
  fairLogDetails: {}, 
  getFairLoading: false, // 单独的getFair加载状态
  getFairLogDetailsLoading: false, // 单独的日志详情加载状态
  error: null 
}
    
const useFairStore = create((set, get) => ({
  ...initialState,   
  getFairs: async (params) => { 
    const endpoint = params?.role === 'admin' ? '/fair/admin' : '/fair/all' 
    set({ getFairLoading: true }) 
    try { 
      const { data } = await axios(`${endpoint}?page=${params?.page || 1}&limit=${params?.limit || 10}`) 
      // 使用函数式更新,避免覆盖其他状态
      set(state => ({
        ...state,
        fairs: data?.data, 
        totalFairs: data?.total, 
        params, 
        error: null
      }))
    } catch (e) { 
      set({ error: e }) 
    } finally { 
      set({ getFairLoading: false }) 
    } 
  },
  getFairLogDetails: async (params, reqBody = {}) => { 
    set({ getFairLogDetailsLoading: true })
    try {
      const { fairId } = params
      const response = await axios.post(`/report/fair-statistics/${fairId}`, reqBody)
      set(state => ({
        ...state,
        fairLogDetails: response?.data,
        error: null
      }))
    } catch (e) {
      set({ error: e })
      return Promise.reject({ 
        code: e?.response?.status, 
        message: e?.response?.data?.message 
      })
    } finally {
      set({ getFairLogDetailsLoading: false })
    }
  },
  // 修正cleanStore,重置所有状态
  cleanStore: () => set(initialState)
}))

2. 组件中使用对应loading状态

FairDetails/index.js修改

const { fairId } = useParams()
const [selectedFair, getFair, getFairLoading, cleanStore] = useFairStore( 
  (state) => [state.selectedFair, state.getFair, state.getFairLoading, state.cleanStore], 
  shallow
)
    
useEffect(() => { 
  getFair(fairId)
  return () => cleanStore()
}, [fairId])

...
    
if (getFairLoading) return <ComponentSpinner />
    
return (
  // 包含LogDetails组件的内容
)

LogDetails.js修改

const [selectedFair, getFairLogDetails, fairLogDetails, getFairLogDetailsLoading] = useFairStore( 
  (state) => [state.selectedFair, state.getFairLogDetails, state.fairLogDetails, state.getFairLogDetailsLoading], 
  shallow 
)

// 增加条件判断,避免重复请求
useEffect(() => {
  if (!selectedFair?._id || Object.keys(fairLogDetails).length > 0) return
  getFairLogDetails({fairId: selectedFair._id})
}, [selectedFair?._id, fairLogDetails])
 
return (
  // 可根据getFairLogDetailsLoading添加局部加载状态
  <div>
    {getFairLogDetailsLoading && <small>加载中...</small>}
    {/* 日志详情内容 */}
  </div>
)

3. 优化selectedFair的引用稳定性(可选)

如果getFair返回的selectedFair内容不变时不需要更新引用,可以在set的时候做浅比较:

// 在getFairs的try块中
set(state => {
  if (shallowEqual(state.selectedFair, data?.data)) {
    return state // 内容不变则不更新,保持引用稳定
  }
  return {
    ...state,
    selectedFair: data?.data,
    // 其他状态更新
  }
})

这里需要引入shallowEqual工具(可自行实现或从react-redux等库引入)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 00:18:12