- 前端
- 缓存
【免费下载链接】swr
React Hooks for Data Fetching
导读
本指南基于 SWR 官方仓库中的 basic-typescript 示例,讲解如何用 TypeScript 为 SWR 的数据请求链路引入完整的类型约束:从带泛型的 fetcher 函数,到useSWR声明式地标注响应数据类型,再到 Next.js 动态路由页面的数据结构声明。读完本文,你将掌握在 Next.js + SWR + TypeScript 项目里搭建一条"从接口返回到 UI 渲染全程类型安全"的请求链路的完整做法,并理解 SWR 内部泛型推断的底层原理。
一、示例定位:官方 TypeScript 入门模板
仓库根目录下的 examples/basic-typescript/README.md 明确给出了该示例的核心目标:
Show how to use the basic example along with TypeScript to type the data received from SWR.
即:在 SWR 基础用法之上叠加 TypeScript,为 SWR 获取到的数据提供类型标注。它与纯 JavaScript 版本的 examples/basic 示例形成对照——后者只演示"在两个页面从 API 获取数据"的基本用法,而本示例的全部价值就在于给这条请求链路加上类型。
整个示例是一个最小可运行的 Next.js 应用,由四个文件组成:
| 文件 | 职责 |
|---|---|
| libs/fetch.ts | 带泛型的类型安全 fetcher |
| pages/index.tsx | 首页,请求项目列表(string[]) |
| pages/[user]/[repo].tsx | 动态路由页,请求 GitHub 仓库统计数据(对象结构) |
| pages/api/data.ts | Next.js API 路由,模拟慢速接口 |
下面从运行方式、fetcher 设计、页面数据消费三个层面逐一展开。
二、快速运行:下载、安装与启动
2.1 下载示例
官方 README 提供了一条基于 curl + tar 的下载命令,直接从 SWR 仓库的主分支中剥离出该示例目录:
curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic-typescript cd basic-typescript命令说明:--strip=2会去掉压缩包内swr-main/examples/basic-typescript/的前两级目录,将示例内容直接解压到当前目录。
2.2 安装与启动
README 同时给出了 yarn 与 npm 两种等价方式:
yarn yarn dev # 或 npm install npm run dev对应到 examples/basic-typescript/package.json 中的 scripts 配置:
"scripts": { "dev": "next", "start": "next start", "build": "next build" }三个脚本分别对应开发模式(next)、生产运行(next start,需先执行 build)与生产构建(next build)。
2.3 依赖组成
依赖清单展示了该示例的技术栈组合:
"dependencies": { "next": "latest", "react": "latest", "react-dom": "latest", "swr": "latest" }, "devDependencies": { "@types/node": "16.7.2", "@types/react": "17.0.19", "typescript": "4.3.5" }要点:
- 运行时依赖全部取最新版,其中
swr直接依赖当前仓库的发布版本; - TypeScript 相关类型包放在 devDependencies(
@types/node、@types/react、typescript),类型标注只影响编译期,不参与运行时打包; - 注意这里没有独立的
@types/swr,因为 SWR 自带的类型定义已足够完整,这也是"官方示例不需要额外类型包"这一事实的最好证明。
2.4 TypeScript 编译配置
examples/basic-typescript/tsconfig.json 是一份标准的 Next.js + TypeScript 配置,其中与类型安全直接相关的关键项:
"compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }值得注意的两点:
strict: true开启全部严格模式选项,确保 fetcher 与页面中的类型标注真正生效、不留类型盲区;noEmit: true表明该配置仅供类型检查,实际编译产物由 Next.js 的打包链路负责,与next-env.d.ts配合即可满足开发时类型检查需求(next-env.d.ts 由 Next.js 自动维护,官方注明"不应手动编辑")。
三、核心一:带泛型的类型安全 fetcher
3.1 示例中的 fetcher 实现
libs/fetch.ts 是整个示例类型安全的起点:
export default async function fetcher<JSON = any>( input: RequestInfo, init?: RequestInit ): Promise<JSON> { const res = await fetch(input, init) return res.json() }逐行拆解:
<JSON = any>泛型默认值:fetcher 声明了一个名为JSON的泛型(名字取自"JSON 响应体"的语义,与内置JSON对象并无冲突),默认值为any。这意味着:调用方若不指定类型,行为退化为普通 JavaScript;一旦调用方通过泛型或上下文推断给出类型,整个返回值的Promise包装层就拥有了精确类型;- 参数类型:
input: RequestInfo与init?: RequestInit直接复用 DOM 标准类型(即fetch()原生签名),因此这个 fetcher 能无缝兼容任意 URL 字符串或Request对象; - 返回值:
Promise<JSON>,即"响应体反序列化结果"被类型化为调用方声明的数据类型。
这段实现与其 JavaScript 版对照物 examples/basic/libs/fetch.js 在运行时行为上完全一致(都是fetch → res.json()),差异全部集中在类型层——这正是"同样的逻辑,多一份类型保障"的直观体现。
3.2 泛型是如何向下流动的:Fetcher 类型定义
从源码层面看,SWR 内部用条件类型为 fetcher 建立了"由 Key 推导参数类型"的约束。在 src/_internal/types.ts 中:
export type Fetcher< Data = unknown, SWRKey extends Key = Key > = SWRKey extends () => (infer Arg) | null | undefined | false ? (arg: Arg) => FetcherResponse<Data> : SWRKey extends null | undefined | false ? never : SWRKey extends infer Arg ? (arg: Arg) => FetcherResponse<Data> : never这套条件类型表达了三条规则:
- Key 是函数:若 Key 是返回值的函数(如
() => '/api/data'),fetcher 的参数类型就是该函数的返回值类型Arg; - Key 是空值:若 Key 是
null | undefined | false,fetcher 类型为never——因为此时 SWR 根本不会发起请求(对应useSWR(null, fetch)的"禁用请求"场景); - Key 是普通值:否则 fetcher 的参数类型就是 Key 本身。
FetcherResponse<Data>(定义于 src/_internal/types.ts)即Data | Promise<Data>,允许 fetcher 返回数据本身或一个 Promise。因此示例中fetcher的Promise<JSON>返回类型天然满足 SWR 的FetcherResponse约束。
3.3 从useSWR泛型到响应类型
SWR 的公开入口 src/index/index.ts 将 src/index/use-swr.ts 中的useSWRHandler以useSWR<Data = any, Error = any>的签名导出。Data与Error两个泛型会一路传递到 hook 的返回值SWRResponse<Data, Error>中,最终让data、error、mutate等都拥有对应类型。
这意味着使用方只需要在调用useSWR时声明Data泛型(通常配合fetcher的返回类型),就能让编译器在data.xxx访问处做完整的属性存在性校验——这正是下一节页面代码演示的用法。
四、核心二:页面中的类型化数据消费
4.1 首页:请求string[]列表
pages/index.tsx 展示了最简单的类型标注:
import Link from 'next/link' import fetch from '../libs/fetch' import useSWR from 'swr' export default function HomePage() { const { data } = useSWR<string[]>('/api/data', fetch) const { data: data2 } = useSWR(null, fetch) return ( <div style={{ textAlign: 'center' }}> <h1>Trending Projects</h1> {data2} <div> {data ? data.map(project => ( <p key={project}> <Link href="/[user]/[repo]" as={`/${project}`}> {project} </Link> </p> )) : 'loading...'} </div> </div> ) }两个值得深入解读的细节:
细节一:useSWR<string[]>显式泛型
useSWR<string[]>('/api/data', fetch)告诉 SWR:/api/data返回的是字符串数组。于是:
data.map(project => ...)中project被推断为string,key={project}与 Link 的as={/${project}}都获得类型校验;- 渲染分支
data ? ... : 'loading...'中,data被收窄为非空数组,这正是 SWR 的data在无缓存时可能为undefined的运行时事实在类型层的反映。
细节二:useSWR(null, fetch)的条件请求写法
第二行useSWR(null, fetch)把 Key 显式设为null,这是 SWR 官方的"禁用请求"惯用法。回到 src/_internal/types.ts 的Fetcher条件类型,null会让 fetcher 类型变为never,类型系统因此知道"这个 hook 永远不会发起网络请求"。同时data2类型为string | undefined(本示例中未赋值,实际渲染为空)。这种写法常用于"等待用户输入/条件满足后再请求"的场景。
4.2 动态路由页:请求结构化对象
pages/[user]/[repo].tsx 演示了对复杂对象结构的类型化:
import Link from 'next/link' import fetch from '../../libs/fetch' import useSWR from 'swr' export default function Repo() { const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' const { data } = useSWR<{ forks_count: number stargazers_count: number watchers: number }>('/api/data?id=' + id, fetch) return ( <div style={{ textAlign: 'center' }}> <h1>{id}</h1> {data ? ( <div> <p>forks: {data.forks_count}</p> <p>stars: {data.stargazers_count}</p> <p>watchers: {data.watchers}</p> </div> ) : ( 'loading...' )} <br /> <br /> <Link href="/">Back</Link> </div> ) }解读:
- 内联对象类型:泛型参数直接写为对象字面量类型
{ forks_count: number; stargazers_count: number; watchers: number },无需额外定义 interface。访问data.forks_count等字段时,编译器会校验属性是否存在且类型为number; - 模板字符串 Key:
'/api/data?id=' + id使 Key 携带查询参数,SWR 会为每个不同的id建立独立缓存条目,配合下方 API 路由可看到"按仓库分别缓存"的效果; - SSR 安全处理:
typeof window !== 'undefined'判断避免在服务端渲染时访问window(此时id为空字符串,请求路径为/api/data?id=)。
4.3 背后的运行时行为:从 useSWRHandler 看数据流
类型标注的运行时载体,是 src/index/use-swr.ts 中useSWRHandler的核心流程:_key首先经过serialize得到序列化后的key与传给 fetcher 的fnArg,然后基于 cache 订阅状态,按revalidateOnMount、revalidateIfStale等配置决定是否立即发起请求(src/index/use-swr.ts)。
实际发起请求时,fetcher 被以currentFetcher(fnArg)的方式调用,其结果与时间戳一起存入全局FETCH表,用于请求去重(src/index/use-swr.ts)。同一个页面中多个组件对同一 Key 调用useSWR时只会触发一次网络请求——这是类型安全之外,SWR 带来的另一个核心收益(自动去重与缓存共享)。
五、配套 API 路由:模拟慢速接口
pages/api/data.ts 是 Next.js API 路由,负责提供两个接口:
import { NextApiRequest, NextApiResponse } from 'next' const projects = [ 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' ] export default function api(req: NextApiRequest, res: NextApiResponse) { if (req.query.id) { // a slow endpoint for getting repo data fetch(`https://api.github.com/repos/${req.query.id}`) .then(resp => resp.json()) .then(data => { setTimeout(() => { res.json(data) }, 2000) }) return } setTimeout(() => { res.json(projects) }, 2000) }设计意图非常清晰:
- 无
id参数:返回写死的项目名数组projects,对应首页useSWR<string[]>的数据源; - 带
id参数:作为服务端代理,转发请求到 GitHub 的https://api.github.com/repos/${req.query.id},把响应原样回传给浏览器,对应动态路由页的对象数据源; - 两处
setTimeout(..., 2000):刻意制造 2 秒延迟,模拟真实网络环境。这正好用来观察 SWR 的加载态表现——页面代码中的'loading...'分支在请求完成前会一直渲染,2 秒后数据到达并触发 UI 更新。
提示:该 API 路由直接依赖外部的 GitHub API。在无网络环境中,服务端代理请求会失败;此时可自行将
/api/data的响应替换为本地 mock 数据,不影响前端类型化演示的效果。
六、从 basic 到 basic-typescript:类型化前后的差异
将 examples/basic/libs/fetch.js 与 examples/basic-typescript/libs/fetch.ts 对照,可以看到类型化带来的三处差异:
| 维度 | 纯 JS 版 | TS 版 |
|---|---|---|
| fetcher 参数 | 隐式...args,任意类型 | input: RequestInfo、init?: RequestInit,DOM 标准类型 |
| 返回值 | 无约束 | Promise<JSON>,泛型可推导 |
| useSWR 调用 | useSWR('/api/data', fetch),data为隐式any | useSWR<string[]>(...),data.map中的元素为string |
这正是 SWR 官方刻意维持的一对镜像示例:运行时行为零差异,开发期体验天壤之别——TS 版让所有数据访问点都处于编译器监督之下,接口结构调整时会立刻在 IDE 中暴露类型错误,而不是等到运行时才出错。
七、延伸实践:把类型安全用到更大的工程里
在掌握示例之后,可以按以下路线将其推广到真实项目:
- 抽取接口类型:将内联对象类型(如
{ forks_count: number })提取为interface或type,配合fetcher<JSON>的泛型,做到"接口返回结构一处定义、多处复用"; - 复用同一 fetcher:
libs/fetch.ts的 fetcher 不绑定任何具体 API,可在全项目共享;换用 axios 时只需保持同样的泛型签名fetchData<T>(...args): Promise<T>,页面代码无需改动; - 合理利用条件请求:
useSWR(null, fetch)写法适合"依赖登录态、表单输入等条件才请求"的场景,配合Fetcher条件类型可在类型层面阻止对空 Key 的误调用; - 关注 strict 配置:保持
tsconfig.json的strict: true,让泛型标注真正生效;若项目中存在历史 JS 文件,allowJs: true允许渐进式迁移。
八、小结
本示例虽然规模极小,却完整覆盖了"TypeScript × SWR × Next.js"三者结合的最小闭环:
- fetcher 层:用泛型
Promise<JSON>封装fetch().json(),作为类型入口; - 调用层:
useSWR<Data>显式声明响应类型,使数据消费点全部类型安全; - 源码支撑:SWR 的
Fetcher条件类型(src/_internal/types.ts)与useSWRHandler泛型签名(src/index/use-swr.ts)共同保证了"Key 决定 fetcher 参数、fetcher 返回类型决定 data 类型"的完整推导链路; - 运行验证:
yarn dev启动后访问首页与任一[user]/[repo]路由,即可在 2 秒延迟的接口下观察加载态与类型化数据渲染的完整效果。
这条"类型安全的数据获取链路",就是 SWR 官方为 TypeScript 用户准备的入门台阶,也是将 SWR 引入大型工程时最值得先落地的一层基础设施。
- 前端
- 缓存
【免费下载链接】swr
React Hooks for Data Fetching
相关推荐
Tamagui与TypeScript泛型:构建类型安全的可复用组件
Tamagui与TypeScript泛型:构建类型安全的可复用组件 在React应用开发中,组件复用与类型安全往往难以兼得。Tamagui作为支持React N
UI组件设计系统前端跨平台browserify与TypeScript泛型:复杂数据结构的类型安全处理
browserify与TypeScript泛型:复杂数据结构的类型安全处理 在前端开发中,随着应用复杂度提升,处理嵌套数组、异构对象等复杂数据结构时,JavaS
前端构建CLI开发工具Saucer:现代C++跨平台Webview库终极指南 — 轻量级构建桌面应用新方案
Saucer:现代C++跨平台Webview库终极指南 — 轻量级构建桌面应用新方案 Saucer是一款现代C++跨平台Webview库,能够帮助开发者轻松构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考