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

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

需求拆解

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 开始,把它接进你的联想框或结果列表,你会立刻感受到「组件瘦身」的爽。

相关推荐
65岁退休Coder7 小时前
LangGraph v1.2.9 节点容错策略 & 流式输出 & 持久化记忆管理
后端·python·langchain
ikun_文9 小时前
Django框架路由Router的使用
python·pycharm·django
IvanCodes9 小时前
Python 基础语法(二):字符串与常用操作
python
昭昭日月明9 小时前
LangChain 生态:从链到代理,开发者需要掌握的三大核心
python·langchain·agent
Csvn9 小时前
🐍 Day 8:面向对象编程
后端·python
程序员天天困10 小时前
向量检索不准怎么办:混合检索与 Rerank 重排序召回优化实战
后端·python·ai编程
alphaTao11 小时前
LeetCode 每日一题 2026/8/24-2026/8/30
python·算法·leetcode
苏灿烤鱼11 小时前
当 AI Agent 遇见真实科学环境:深度拆解 Scientific Agent Skills,把"聊天机器人"变成"AI 科学家"
python·开源·agent
张文君11 小时前
ubuntu26.04坏道坏块分区隔离急速版260831-V0.12
linux·python