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

如何为自定义useFetchOnClick钩子添加请求中止功能?

实现请求自动中止,避免重复点击触发多请求

不需要彻底重构现有钩子,只需添加对未完成请求的追踪和自动中止逻辑即可解决问题。核心思路是用useRef保存当前请求的AbortController实例,在新请求发起前中止旧请求,并区分主动中止错误与真实请求错误。

修改后的完整代码

import { useReducer, useRef } from 'react';

const FetchType = {
    FETCH_START: 'FETCH_START',
    FETCH_SUCCESS: 'FETCH_SUCCESS',
    FETCH_ERROR: 'FETCH_ERROR'
}

const defaultState = {
    loading: false,
    error: false,
    success: false,
    nthFetch: 0,
    data: null
}

const fetchReducer = (state , action) => {
    switch(action.type){
        case FetchType.FETCH_START:
            return {
                ...state,
                loading: true,
                error: false,
                success: false,
                nthFetch: state.nthFetch + 1
            }
        case FetchType.FETCH_SUCCESS:
            return {
                ...state,
                loading: false,
                error: false,
                success: true,
                data: action.payload
            }
        case FetchType.FETCH_ERROR:
            return {
                ...state,
                loading: false,
                error: true,
                success: false,
                data: null
            }
        default:
            return state
    }
}

// the responseConversion function is used to convert the response to the desired format
// for example `response => response.json()` or `response => response.blob()`
const useFetchOnClick = (responseConversion) => {
    const [state, dispatch] = useReducer(fetchReducer, defaultState)
    // 用ref跨渲染周期保存当前未完成请求的控制器
    const abortControllerRef = useRef(null)

    const fetchOnClick = (url, options) => {
        // 1. 中止之前未完成的请求
        if (abortControllerRef.current) {
            abortControllerRef.current.abort();
        }

        // 2. 创建新的请求控制器并保存到ref
        const abortController = new AbortController()
        const signal = abortController.signal
        abortControllerRef.current = abortController

        dispatch({ type: FetchType.FETCH_START })
        fetch(url, { ...options, signal })
            .then(responseConversion)
            .then(data => {
                dispatch({ type: FetchType.FETCH_SUCCESS, payload: data })
            })
            .catch(error => {
                // 仅当不是主动中止错误时,才更新错误状态
                if (error.name !== 'AbortError') {
                    dispatch({ type: FetchType.FETCH_ERROR })
                    console.log(error)
                }
            })
            .finally(() => {
                // 请求完成后清空对应控制器,避免内存泄漏
                if (abortControllerRef.current === abortController) {
                    abortControllerRef.current = null
                }
            })

        // 保留手动中止能力,组件可按需调用
        return () => {
            if (abortControllerRef.current === abortController) {
                abortController.abort()
                abortControllerRef.current = null
            }
        }
    }

    return [state, fetchOnClick]
} 

关键修改说明

  • useRef追踪请求状态:用abortControllerRef存储当前未完成请求的控制器,ref的更新不会触发组件重渲染,适合跨周期追踪请求实例。
  • 自动中止旧请求:每次调用fetchOnClick时,先检查是否有未完成的请求,若有则立即中止,避免多个请求同时执行导致状态混乱。
  • 区分中止错误:fetch被中止时会抛出AbortError,此时不更新错误状态,避免给用户展示错误提示。
  • 清理资源:在请求完成(成功/失败/中止)后清空ref,防止残留过期控制器导致内存泄漏。
  • 保留手动中止:仍返回中止函数,组件可在卸载或其他场景主动调用中止请求。

组件使用示例(无需修改)

export default function TestComponent() {
  const [state, fetchOnClick] = useFetchOnClick(response => response.json())

  return (
    <div>
      <p>test component</p>
      <button
        onClick={() => fetchOnClick("https://api.chucknorris.io/jokes/random")}
      >
        Chuck Norris Fact
      </button>
      {
        state.nthFetch === 0 ? <p>Press the button to get started</p> :
        state.loading ? <p>Loading...</p> :
        state.error ? <p>Error</p> :
        <p>{state.data.value}</p>
      }
    </div>
  )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 11:14:55