用 React Hook 封装搜索数据:useSerp 的防抖、缓存与错误处理

给 React 应用接搜索数据(联想词、排名、SERP 快照),如果每个组件各写一套 fetch,代码很快会烂。正确姿势是封装一个 Hook :useSerp------把防抖、缓存、加载态、错误处理都收进去,组件只关心数据。这篇是完整实现,可直接跑。

需求拆解

React 里接搜索数据,组件反复要面对四件事:

  1. 输入防抖:搜索框打字,别每键都发请求
  2. 请求缓存:相同查询别重复调接口
  3. 加载态/错误态:请求中显示什么、失败怎么处理
  4. 竞态处理:快速连输,别让旧请求覆盖新结果

这四件事在组件里写一遍很烦,封装成 Hook 后,组件一行调用。

后端先备好

Key 不能进浏览器(前面写过),所以 Hook 请求的是自己的后端代理:

js 复制代码
// app/api/serp/route.js ------ Next.js 代理
export async function GET(req) {
  const q = new URL(req.url).searchParams.get("q");
  const resp = await fetch("https://api.serpbase.dev/google/search", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.SERPBASE_API_KEY,
    },
    body: JSON.stringify({ q, hl: "zh-cn", gl: "cn" }),
  });
  const data = await resp.json();
  return Response.json({
    organic: data.organic ?? [],
    related: data.related_searches ?? [],
    request_id: data.request_id,
  });
}

useSerp Hook 实现

ts 复制代码
import { useCallback, useEffect, useRef, useState } from "react";

interface SerpState {
  data: { organic: any[]; related: string[] } | null;
  loading: boolean;
  error: string | null;
}

export function useSerp(query: string, delayMs = 300) {
  const [state, setState] = useState<SerpState>({
    data: null, loading: false, error: null,
  });
  const cacheRef = useRef(new Map<string, any>());
  const timerRef = useRef<number>();
  const seqRef = useRef(0);

  useEffect(() => {
    if (!query.trim()) {
      setState({ data: null, loading: false, error: null });
      return;
    }

    // 防抖:清除上一次定时器
    clearTimeout(timerRef.current);

    timerRef.current = window.setTimeout(async () => {
      const key = query.trim();

      // 缓存命中:直接返回,不发请求
      if (cacheRef.current.has(key)) {
        setState({ data: cacheRef.current.get(key), loading: false, error: null });
        return;
      }

      const mySeq = ++seqRef.current;   // 竞态序号
      setState((s) => ({ ...s, loading: true, error: null }));

      try {
        const resp = await fetch(`/api/serp?q=${encodeURIComponent(key)}`);
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const data = await resp.json();

        // 竞态检查:只有最新一次请求才能写入
        if (mySeq === seqRef.current) {
          cacheRef.current.set(key, data);
          setState({ data, loading: false, error: null });
        }
      } catch (e: any) {
        if (mySeq === seqRef.current) {
          setState((s) => ({ ...s, loading: false, error: e.message }));
        }
      }
    }, delayMs);

    return () => clearTimeout(timerRef.current);
  }, [query, delayMs]);

  return state;
}

四个能力全在这一个 Hook 里:

  • 防抖 :clearTimeout + setTimeout(delayMs)
  • 缓存 :useRef(Map),相同 query 直接命中
  • 加载/错误态 :state 里管理
  • 竞态处理 :seqRef 序号,只有最新请求能写入状态

组件用法:极简

tsx 复制代码
function SuggestionBox() {
  const [input, setInput] = useState("");
  const { data, loading, error } = useSerp(input, 300);   // 一行调用

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)}
             placeholder="输入关键词,看真实联想词" />

      {loading && <p>加载中...</p>}
      {error && <p style={{ color: "red" }}>出错了: {error}</p>}

      {data && (
        <ul>
          {data.related.map((w) => <li key={w}>{w}</li>)}
          {data.organic.slice(0, 5).map((r) => (
            <li key={r.link}><a href={r.link}>{r.title}</a></li>
          ))}
        </ul>
      )}
    </div>
  );
}

组件里没有 fetch、没有防抖逻辑、没有竞态处理------全在 Hook 里。这就是封装的价值。

进阶:SSR 场景(Next.js)

服务端组件想用搜索结果做 SEO 友好渲染,直接 fetch 代理接口:

tsx 复制代码
// app/page.tsx ------ 服务端组件
export default async function Page({ params }: { params: { q: string } }) {
  const resp = await fetch(`http://localhost:3000/api/serp?q=${params.q}`,
    { cache: "no-store" });
  const data = await resp.json();

  return (
    <div>
      <h1>{params.q} 的搜索结果</h1>
      {data.organic.map((r) => (
        <a key={r.link} href={r.link}>{r.title}</a>
      ))}
    </div>
  );
}

SSR 场景数据直接在服务端拿好渲染,SEO 友好。

成本与注意

  • /google/search 每次 1 credits;Hook 的缓存挡住同会话重复查询
  • 注册送 100 次免费搜索,开发测试够
  • 标准包 10买2万次(10 买 2 万次( 10买2万次(0.50/千次),永不过期
  • 缓存是内存级(组件卸载即清),够防「同会话重复」;跨会话用 KV 持久缓存

小结

用 Hook 封装搜索数据,本质是把「四件烦心事」(防抖、缓存、加载/错误、竞态)从组件里抽走,让组件只消费数据。useSerp 一套实现,全项目复用。

接口参数细节在 SerpBase 官方文档。建议从复制这个 Hook 开始,把它接进你的联想框或结果列表,你会立刻感受到「组件瘦身」的爽。

相关推荐
蚰蜒螟33 分钟前
从内核启动到 Java NIO:一次 socket accept 的源码串联之旅
java·python·nio
高洁011 小时前
AI赋能产业升级与中美未来经济赛道博弈
人工智能·python·深度学习·机器学习·transformer
夜晚回家1 小时前
Python 原始字符串详解 | r 前缀到底让什么不再转义
开发语言·python
用户019027581612 小时前
如何用 Python 计算 EMV 简易波动指标?
python
第25小时记录2 小时前
[2026] Linux安装教程
运维·python
前端 贾公子3 小时前
LangGraph == 图的状态(State)管理 (中)
开发语言·数据库·python
the局外人4 小时前
FastAPI中jwt的基本使用与实践
后端·python·fastapi
mklpo1474 小时前
为不支持sse4.2命令集的老旧设备制作Win11 26H2系统镜像,2026-10-11。
运维·windows·python
GoldenSpider.AI4 小时前
用 AI 改流程之前先做「四桶」体检:9 次返工里 8 次靠规则就能消掉(附 Python 发布前校验器)
人工智能·python·自动化·ai智能体