为何useEffect调用Zustand的Axios数据获取函数多次触发?如何解决?
问题分析与解决方案
问题现象
我在Zustand Store中通过Axios实现了数据获取逻辑:
import create from 'zustand' import axios from "axios"; export const useStore = create((set) => ({ pokemons: [], eachPokemon: {}, getPokemons: async () => { const response = await axios.get('https://pokeapi.co/api/v2/pokemon') set({ pokemons: await response.data.results }) }, getPokemon: async(pokemonURL) => { const response = await axios.get(pokemonURL) set({ eachPokemon: await response.data}) } }))
在React组件的useEffect中调用getPokemons时:
import React, {useEffect} from 'react' import { useStore } from '../../store/store' import ItemDetails from './ItemDetails' function Items() { const {pokemons, getPokemons} = useStore((state) => ({ pokemons: state.pokemons, getPokemons: state.getPokemons })) useEffect(() => { getPokemons(); console.log(pokemons) }, [getPokemons]) return ( <> Some code </> ) } export default Items
发现useEffect里的console.log执行了两次;如果把console.log放到useEffect外:
import React, {useEffect} from 'react' import { useStore } from '../../store/store' import ItemDetails from './ItemDetails' function Items() { const {pokemons, getPokemons} = useStore((state) => ({ pokemons: state.pokemons, getPokemons: state.getPokemons })) useEffect(() => { getPokemons(); }, [getPokemons]) console.log(pokemons) return ( <> Some code </> ) } export default Items
控制台会多次输出pokemons数组:先输出空数组,之后多次打印包含20条数据的数组。想知道原因,以及如何实现只获取一次数据。
原因解析
- React Strict Mode的开发环境行为:开发环境下,React Strict Mode会强制执行两次
useEffect等生命周期钩子,目的是检测未清理的副作用、重复渲染等潜在问题,这是useEffect内日志执行两次的直接原因。 - 组件重渲染与依赖不稳定:
- 调用
getPokemons后,Zustand更新pokemons状态,组件会因状态变化触发重渲染,每次重渲染都会执行组件内的console.log,所以会多次输出数组。 - 当前的Zustand selector每次返回新对象,导致
getPokemons的引用在组件每次渲染时都不一样,useEffect会因为依赖项变化额外触发执行。
- 调用
解决方法
1. 稳定getPokemons的引用,避免不必要的useEffect触发
Zustand的selector如果返回新对象,会导致依赖项引用变化,可通过两种方式解决:
// 方法一:拆分selector,单独获取每个状态/方法 const pokemons = useStore(state => state.pokemons) const getPokemons = useStore(state => state.getPokemons) // 方法二:使用shallow比较,避免新对象引用 import { shallow } from 'zustand/shallow' const {pokemons, getPokemons} = useStore(state => ({ pokemons: state.pokemons, getPokemons: state.getPokemons }), shallow)
2. 确保只发起一次数据请求
方案一:组件层控制请求时机
在useEffect中判断数据是否已存在,避免重复请求:
useEffect(() => { if (pokemons.length === 0) { getPokemons() } }, [getPokemons, pokemons])
方案二:Store层控制(更推荐)
在Store的getPokemons方法内添加判断,只要数据已存在就不再发起请求:
export const useStore = create((set, get) => ({ pokemons: [], eachPokemon: {}, getPokemons: async () => { const currentPokemons = get().pokemons // 已有数据直接返回,避免重复请求 if (currentPokemons.length > 0) return const response = await axios.get('https://pokeapi.co/api/v2/pokemon') set({ pokemons: response.data.results }) }, getPokemon: async(pokemonURL) => { const response = await axios.get(pokemonURL) set({ eachPokemon: response.data}) } }))
3. 关于开发环境的两次执行
生产环境下React Strict Mode不会触发两次执行,上线后useEffect只会执行一次。开发环境中如果不想看到两次执行,可以暂时关闭Strict Mode,但不建议这么做——它能帮你提前发现潜在的代码问题。
内容的提问来源于stack exchange,提问作者Young Jun Joo
相关产品推荐
相关产品推荐

