给 React 应用接搜索数据(联想词、排名、SERP 快照),如果每个组件各写一套 fetch,代码很快会烂。正确姿势是封装一个 Hook :useSerp------把防抖、缓存、加载态、错误处理都收进去,组件只关心数据。这篇是完整实现,可直接跑。
需求拆解
React 里接搜索数据,组件反复要面对四件事:
- 输入防抖:搜索框打字,别每键都发请求
- 请求缓存:相同查询别重复调接口
- 加载态/错误态:请求中显示什么、失败怎么处理
- 竞态处理:快速连输,别让旧请求覆盖新结果
这四件事在组件里写一遍很烦,封装成 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万次(0.50/千次),永不过期
- 缓存是内存级(组件卸载即清),够防「同会话重复」;跨会话用 KV 持久缓存
小结
用 Hook 封装搜索数据,本质是把「四件烦心事」(防抖、缓存、加载/错误、竞态)从组件里抽走,让组件只消费数据。useSerp 一套实现,全项目复用。
接口参数细节在 SerpBase 官方文档。建议从复制这个 Hook 开始,把它接进你的联想框或结果列表,你会立刻感受到「组件瘦身」的爽。