React useEffect 异步的实现示例

 更新时间:2026年07月05日 15:10:06   作者:Csvn  
本文主要介绍了React useEffect 异步的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

问题场景

你在做一个搜索下拉组件,用户输入关键词后自动请求后端接口:

// ❌ 常见写法,埋了竞态地雷
const SearchSuggest = ({ query }: { query: string }) => {
  const [suggestions, setSuggestions] = useState<string[]>([])

  useEffect(() => {
    fetch(`/api/suggest?q=${query}`)
      .then(res => res.json())
      .then(data => setSuggestions(data))
  }, [query])

  return <ul>{suggestions.map(s => <li key={s}>{s}</li>)}</ul>
}

看起来很对?测试一下:

用户输入 "rea" → 请求 A 发出(耗时 300ms) 用户继续输入 "react" → 请求 B 发出(耗时 100ms) 用户又输入 "react h" → 请求 C 发出(耗时 200ms)

结果:

请求 B 先回来(100ms) → setSuggestions(["react"])
请求 A 才回来(300ms) → setSuggestions(["rea"])   ← ❌ 把正确的数据覆盖了!
请求 C 回来(200ms) → setSuggestions(["react hook"])

用户明明搜的是 "react hook",结果 2 秒内页面闪现三组数据后,最终显示了根本不完整的 ["rea"]。这对用户体验是灾难性的。

原因分析

问题的本质是 异步请求的响应顺序不可控

  • 请求 B(最新)先返回 → 更新状态 ✅
  • 请求 A(过期)后返回 → 覆盖状态 ❌

React 不会替你判断"哪个响应是新哪个响应旧"。setSuggestions 是个纯函数,谁来调用它,它就被谁覆盖。

这种现象叫做 Race Condition(竞态条件)——多个异步操作竞争同一个状态写入权,后发先至造成数据不一致。

不止是搜索场景,分页列表切换、Tab 切换加载、详情页路由跳转……只要组件 Props/State 变化触发新请求,而旧请求还在 pending,就会出现竞态。

解决方案(3 种,由简到优)

方案一:useEffect Cleanup + 布尔守卫(最基础)

useEffect(() => {
  let ignore = false  // 👈 关键守卫变量

  fetch(`/api/suggest?q=${query}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) {  // 👈 组件未卸载/query未变化才写入
        setSuggestions(data)
      }
    })

  return () => {
    ignore = true     // 👈 组件卸载或 query 变化时标记过期
  }
}, [query])

原理: 每次 query 变化时,上一次的 cleanup 先执行,把 ignore 置为 true。旧请求的 .then 即便执行了也不会修改状态。

⚠️ 但这个方案只能阻止卸载后的回调,如果旧请求超慢、新请求更快,旧请求仍然可能在新请求之前执行 .then 并写入。单纯 ignore 不够可靠。

方案二:AbortController 取消请求(推荐生产使用)

useEffect(() => {
  const controller = new AbortController()   // 👈 创建取消控制器

  fetch(`/api/suggest?q=${query}`, {
    signal: controller.signal                // 👈 绑定 signal
  })
    .then(res => res.json())
    .then(data => setSuggestions(data))
    .catch(err => {
      if (err.name !== 'AbortError') {       // 👈 忽略主动取消的错误
        console.error('请求失败:', err)
      }
    })

  return () => {
    controller.abort()                       // 👈 组件卸载/query变化时取消上次请求
  }
}, [query])

为什么更好?

方案优点缺点
ignore 守卫简单易懂请求仍在背后运行,浪费带宽
AbortController实际取消 HTTP 请求,节省资源需要浏览器支持(>Chrome 66,无需担心)

同时 AbortController 是 100% 可靠的——旧请求的 promise 被 reject,永远不可能再调用 setSuggestions。

用 fetch 默认支持,不需要额外库。如果你用 axios,等价的是 CancelToken(v0.22+ 用 AbortSignal)。

方案三:自定义 Hook 封装(一劳永逸)

把竞态处理封装进 useFetch,全项目通用:

// useFetch.ts
import { useEffect, useState, useRef } from 'react'

function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null } {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    let ignore = false
    const controller = new AbortController()
    setLoading(true)

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        return res.json() as Promise<T>
      })
      .then(res => {
        if (!ignore) {
          setData(res as T)
          setError(null)
        }
      })
      .catch(err => {
        if (err.name === 'AbortError') return  // 静默忽略
        if (!ignore) {
          setError(err)
          setData(null)
        }
      })
      .finally(() => {
        if (!ignore) setLoading(false)
      })

    return () => {
      ignore = true
      controller.abort()
    }
  }, [url])

  return { data, loading, error }
}

使用:

const SearchSuggest = ({ query }: { query: string }) => {
  const { data, loading } = useFetch<string[]>(`/api/suggest?q=${query}`)

  if (loading) return <p>加载中…</p>
  return <ul>{data?.map(s => <li key={s}>{s}</li>)}</ul>
}

进一步优化: 配合 lodash.debounce 或 useDeferredValue 做防抖,减少请求频率,效果更佳。

实操代码:完整搜索组件(含防抖 + AbortController)

import { useState, useEffect, useRef, useMemo } from 'react'
import { debounce } from 'lodash-es'

const SearchPage = () => {
  const [input, setInput] = useState('')
  const [query, setQuery] = useState('')
  const [suggestions, setSuggestions] = useState<string[]>([])
  const [loading, setLoading] = useState(false)

  // 防抖:用户停止输入 300ms 后设为正式查询词
  const debouncedSetQuery = useMemo(
    () => debounce((val: string) => setQuery(val), 300),
    []
  )

  useEffect(() => {
    debouncedSetQuery(input)
    return () => debouncedSetQuery.cancel()
  }, [input])

  // 请求竞态保护
  useEffect(() => {
    if (!query) {
      setSuggestions([])
      return
    }

    const controller = new AbortController()
    setLoading(true)

    fetch(`/api/suggest?q=${encodeURIComponent(query)}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => {
        setSuggestions(data)
        setLoading(false)
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err)
          setLoading(false)
        }
      })

    return () => controller.abort()
  }, [query])

  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="输入搜索关键词…"
      />
      {loading && <span>搜索中…</span>}
      <ul>
        {suggestions.map((s, i) => (
          <li key={i}>{s}</li>
        ))}
      </ul>
    </div>
  )
}

要点总结

  • 竞态的本质:异步请求后发先至,旧请求覆盖新响应
  • ignore 守卫最低成本,适合简单场景;AbortController 才是生产级方案,能真正取消网络请求
  • 自定义 useFetch Hook 将竞态处理集中封装,团队统一使用避免重复踩坑
  • 配合 防抖(Debounce) 可以减少无效请求,请求量降低 80%+
  • 搜索/分页/Tab 切换/路由跳转——所有"频繁变化参数 + 异步请求"的场景都需要考虑竞态处理
  • React 18 的 Suspense + useTransition 未来可能改变这个模式,但传统 useEffect 取数依然广泛存在,务必掌握

到此这篇关于React useEffect 异步的实现示例的文章就介绍到这了,更多相关React useEffect 异步内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • React子组件调用父组件方法获取的数据不是最新值的解决方法

    React子组件调用父组件方法获取的数据不是最新值的解决方法

    这篇文章主要介绍了React子组件调用父组件方法获取的数据不是最新值的解决方法,文中通过代码示例介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-09-09
  • react antd-mobile ActionSheet+tag实现多选方式

    react antd-mobile ActionSheet+tag实现多选方式

    这篇文章主要介绍了react antd-mobile ActionSheet+tag实现多选方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • React项目中使用zustand状态管理的实现

    React项目中使用zustand状态管理的实现

    zustand是一个用于状态管理的小巧而强大的库,本文主要介绍了React项目中使用zustand状态管理的实现,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • React State状态与生命周期的实现方法

    React State状态与生命周期的实现方法

    这篇文章主要介绍了React State状态与生命周期的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • React Native全面屏状态栏和底部导航栏适配教程详细讲解

    React Native全面屏状态栏和底部导航栏适配教程详细讲解

    最近在写 React Native 项目,调试应用时发现顶部状态栏和底部全面屏手势指示条区域不是透明的,看起来很难受。研究了一下这个问题,现在总结一下解决方案,这篇文章主要介绍了React Native全面屏状态栏和底部导航栏适配教程
    2023-01-01
  • React避免不必要的重新渲染的方法示例

    React避免不必要的重新渲染的方法示例

    构建高性能 React 应用程序的关键之一是避免不必要的重新渲染,React 的渲染引擎是高效的,但防止在不需要的地方重新渲染仍然至关重要,在这篇文章中,我们将介绍常见错误以及如何避免它们,需要的朋友可以参考下
    2024-10-10
  • React 中使用 react-i18next 国际化的过程(react-i18next 的基本用法)

    React 中使用 react-i18next 国际化的过程(react-i18next 的基本用法)

    i18next 是一款强大的国际化框架,react-i18next 是基于 i18next 适用于 React 的框架,本文介绍了 react-i18next 的基本用法,如果更特殊的需求,文章开头的官方地址可以找到答案
    2023-01-01
  • React 实现拖拽功能的示例代码

    React 实现拖拽功能的示例代码

    这篇文章主要介绍了React 实现拖拽功能的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • react组件实例属性props实例详解

    react组件实例属性props实例详解

    这篇文章主要介绍了react组件实例属性props,本文结合实例代码给大家简单介绍了props使用方法,代码简单易懂,需要的朋友可以参考下
    2023-01-01
  • React hook超详细教程

    React hook超详细教程

    Hook是React16.8的新增特性。它可以让你在不编写class的情况下使用state以及其他的React特性,这篇文章主要介绍了React hook的使用
    2022-10-10

最新评论