React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React useEffect 异步

React useEffect 异步的实现示例

作者: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"]。这对用户体验是灾难性的。

原因分析

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

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>
  )
}

要点总结

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

您可能感兴趣的文章:
阅读全文