React函数组件抽离fetch函数报无效Hook调用错误如何解决?
错误产生原因
- 违反React Hook调用规则:你在抽离的
FetchCityData函数中使用了useStateHook,但你是在按钮的onClick事件回调中调用这个函数的,不符合Hook只能在React函数组件顶层作用域、或自定义Hook顶层作用域调用的规则,因此触发非法Hook调用报错。 - 存在逻辑漏洞:
FetchCityData中直接使用了未作为参数传入的city变量,且内部声明的状态和组件传入的payload/setPayload完全割裂,状态无法互通,即使解决Hook报错问题逻辑也无法正常运行。
正确的抽离方案(满足单元测试需求)
方案1:抽离纯请求逻辑(推荐,测试成本最低)
把和React状态无关的纯请求逻辑单独抽离,状态保留在组件内。抽离后的请求函数没有任何React依赖,属于纯函数,单元测试时只需mock fetch即可,无需挂载React组件。
// 可单独导出,无任何React依赖,可直接单元测试 export const fetchCityWeather = (city) => { const options = { method: `POST`, }; return fetch(`/api/weather?city=${city}`, options) .then((response) => { if(response.ok){ return response.json() } throw new Error('Api is not available') }) }
组件中调用逻辑:
import { useState } from 'react' import { fetchCityWeather } from './对应路径' export const ForecastButtons = ({ city }) => { const [payload, setPayload] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const handleFetch = () => { setLoading(true) fetchCityWeather(city) .then(setPayload) .catch(error => { console.error('Error fetching data: ', error) setError(error) }) .finally(() => setLoading(false)) } const location = payload?.location?.name; const currentTemp = payload?.current?.temp_c; return( <div className="sm:col-span-2"> <p className="block text-sm font-medium text-gray-700">Select forecast</p> <button onClick={handleFetch} className="mt-1 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded" type='button'> Today </button> <p key={city?.location?.id} className='my-5'> { location ? `Current weather in ${location} is ${currentTemp} degrees ` : 'Please search for city to see current weather'} </p> </div> ) }
方案2:抽为自定义Hook(适用于多组件复用请求+状态逻辑)
如果需要在多个组件复用整套请求+状态管理逻辑,可以抽为自定义Hook,要求函数名必须以use开头,且仅能在组件顶层调用:
import { useState } from 'react' // 自定义Hook,符合Hook调用规则 export const useCityWeather = (city) => { const [payload, setPayload] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const fetchData = () => { setLoading(true) const options = { method: `POST`, }; fetch(`/api/weather?city=${city}`, options) .then((response) => { if(response.ok){ return response.json().then(setPayload) } throw new Error('Api is not available') }) .catch(error => { console.error('Error fetching data: ', error) setError(error) }) .finally(() => setLoading(false)) } return { payload, error, loading, fetchData } }
组件中调用逻辑:
import { useCityWeather } from './对应路径' export const ForecastButtons = ({ city }) => { // 自定义Hook必须在组件顶层调用 const { payload, fetchData } = useCityWeather(city) const location = payload?.location?.name; const currentTemp = payload?.current?.temp_c; return( <div className="sm:col-span-2"> <p className="block text-sm font-medium text-gray-700">Select forecast</p> <button onClick={fetchData} className="mt-1 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded" type='button'> Today </button> <p key={city?.location?.id} className='my-5'> { location ? `Current weather in ${location} is ${currentTemp} degrees ` : 'Please search for city to see current weather'} </p> </div> ) }
自定义Hook可通过React Hooks测试工具完成单元测试,模拟入参、调用触发函数后断言状态变化即可。
内容的提问来源于stack exchange,提问作者Katharina Schreiber
相关产品推荐
相关产品推荐

