大多数React应用程序与远程数据源通信,以持久化和检索数据记录。现在的Web应用开发团队倾向于使用REST和类似GraphQL的通信模式来实现其远程数据源接口。然后,前端开发团队必须通过他们的React应用程序与各种库进行网络请求,以便在客户端和服务器端之间同步数据。
对于与RESTful服务的通信,最简单的方法是使用内置的Fetch API或组件中的Axios等库来装载类似状态的事件。然后,你必须编写额外的逻辑来实现加载状态的UI增强。最后,为了使你的应用程序更加用户友好,并通过数据缓存、重复的API查询和预获取进行优化,你可能需要编写比你的客户端业务逻辑更多的代码!
这就是像SWR和TanStack Query--以前的React Query--这样的库可以帮助你通过缓存、预取、重复查询和其他各种可用性功能将数据源的状态与React应用的状态同步。
在这篇文章中,我将用一个实际的例子项目来比较SWR和TanStack Query库的功能。
什么是React SWR?
SWR是一个开源的、轻量级的、与TypeScript兼容的库,它提供了几个Hooks,用于在React中通过缓存获取数据。缩写 "SWR "代表State While Re-validate,是HTTPRFC 5861中的一个通用缓存原则。
React SWR于2019年首次通过其v0.1.2公开发布。
突出的特点
这个库提供了以下突出的功能:
特点 | 描述 |
---|---|
轻量级尺寸和高性能 | 根据BundlePhobia的数据,SWR库在gzipped时的重量约为4.2千字节。SWR开发团队专注于性能和轻量级的树形摇动捆绑策略 |
最小化、可配置和可重用的API | SWR还专注于为React开发者提供一个最小的、开发者友好的API,提供性能友好的功能。你可以用一个Hook实现你需要的大部分东西,useSWR 。 |
即使API是最小的,它也可以让你通过一个全局配置和许多Hook选项来调整缓存系统和行为。 |
|
| 为开发者和用户提供的内置功能 | SWR支持分页请求,并提供useSWRInfinite
Hook来实现无限加载。它还可以与React Suspense API、SSG和SSR一起工作,并提供预取、焦点上的重新验证和网络状态的重新获取,如对应用程序用户的可用性增强。 |
使用React SWR
现在我们对SWR在React中优化数据获取的功能有了一个概述,让我们用SWR创建一个样本应用程序,并对其进行评估,以找到与TanStack Query的比较点。
我们可以在客户端用延迟承诺来模拟我们的API后端,以尝试SWR,但这种方法并不能提供真实的数据获取体验。让我们改用Node.js创建一个简单的RESTful API。我们可以通过json-server
包在几秒钟内创建一个RESTful API服务器。
首先,在全球范围内安装json-server
包:
npm install -g json-server
# --- or ---
yarn global add json-server
接下来,将以下内容添加到一个名为db.json
的新文件中:
{
"products": [
{
"id": 1,
"name": "ProX Watch",
"price": 20
},
{
"id": 2,
"name": "Magic Pencil",
"price": 2
},
{
"id": 3,
"name": "RevPro Wallet",
"price": 15
},
{
"id": 4,
"name": "Rice Cooker",
"price": 25
},
{
"id": 5,
"name": "CookToday Oven",
"price": 10
}
]
}
接下来,运行以下命令,在db.json
文件的基础上启动一个RESTful CRUD服务器:
json-server --watch --port=5000 --delay=1000 db.json
现在,我们可以通过以下方式访问我们的CRUD API http://localhost:5000/products
.如果你想的话,你可以用Postman来测试它。在我们的例子中,我们添加了一个1000ms的延迟来模拟网络延迟。
让我们创建一个新的React应用并通过SWR获取数据。如果你已经是SWR的用户,或者你以前试验过SWR,你可以在这个GitHub仓库查看完整的项目,然后继续看下面的TanStack查询部分。
像往常一样,创建一个新的React应用:
npx create-react-app react-swr-example
cd react-swr-example
安装软件包
接下来,用以下命令安装swr
包:
npm install swr
# --- or ---
yarn add swr
我们将在本教程中使用Axios,所以也要安装它,用以下命令。你可以使用任何HTTP请求库或内置的fetch
,因为SWR期望的只是承诺:
npm install axios
# --- or ---
yarn add axios
实现数据提取器和自定义Hooks
我们将通过创建一个简单的产品管理应用来评估SWR,该应用列出一些产品并允许你添加新产品。首先,我们需要在.env
文件中存储我们本地模拟API的基本URL。创建一个名为.env
的新文件并添加以下内容:
REACT_APP_API_BASE_URL = "http://localhost:5000"
接下来,通过在index.js
文件中添加以下内容,在Axios全局配置中使用该基础URL:
import React from 'react';
import ReactDOM from 'react-dom/client';
import axios from 'axios';
import './index.css';
import App from './App';
axios.defaults.baseURL = process.env.REACT_APP_API_BASE_URL;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
我们将在我们的App.js
文件中保留所有的应用程序组件,以保持教程的简单性。在你的App.js
文件中清理一切,并添加以下导入内容:
import React, { useState } from 'react';
import useSWR from 'swr';
import axios from 'axios';
import './App.css';
在这里,我们从swr
,导入useSWR
Hook来获取缓存的数据记录,而不是直接调用Axios函数。
对于获取没有RESTful URL参数的数据,我们通常需要向useSWR
Hook提供两个参数:一个唯一的键(通常是URL)和一个fetcher函数,这是一个返回异步数据的JavaScript函数。
添加下面的代码,其中包括一个fetcher:
function fetcher(url) {
return axios.get(url).then(res => res.data);
}
async function addProduct(product) {
let response = await axios.post('/products', product);
return response.data;
}
这里,fetcher
函数通过Axios异步返回数据,addProduct
函数同样发布产品数据并返回新创建的产品。
现在,我们可以在功能组件中使用useSWR(‘/products’, fetcher)
语句来获取缓存的产品,但SWR开发人员建议使用可重用的自定义Hooks。在App.js
文件中添加以下Hook:
function useProducts() {
const { data, error, mutate } = useSWR('/products', fetcher);
return {
products: data,
isLoading: !data,
isError: !!error,
mutate
};
}
我们的useProducts
自定义Hook输出以下道具:
products
:从API获取数据后的产品数组;如果没有从API获取数据,它就变成undefined
isLoading
:基于API数据的加载指标isError
:一个布尔值,表示加载错误mutate
:一个更新缓存数据的函数,即时反映在用户界面上。
使用SWR来获取数据
现在我们可以使用useProducts
data Hook来更新来自后台数据的用户界面。创建Products
组件来列出所有可用的产品:
function Products() {
const { products, isLoading, isError } = useProducts();
if(isError)
return (
<div>Unable to fetch products.</div>
);
if(isLoading)
return (
<div>Loading products...</div>
);
return (
products.map((product) => (
<div key={product.id} className="product-item">
<div>{product.name}</div>
<div>${product.price}</div>
</div>
))
);
}
Products
组件根据useProducts
Hook道具有条件地进行渲染。如果你在许多组件中多次使用这个Hook,SWR将只发起一个HTTP请求,根据重复请求功能,然后,获取的数据将通过useProducts
Hook与所有组件共享,用于渲染过程。
突变缓存数据并使请求失效
创建一个名为AddProduct
的组件,用以下代码实现添加新产品的方法:
function AddProduct({ goToList }) {
const { products, mutate } = useProducts();
const [product, setProduct] = useState({
id: products.length + 1,
name: '',
price: null
});
const [disabled, setDisabled] = useState(true);
async function handleAdd() {
goToList();
mutate(async () => {
return [...products, await addProduct(product)]
}, { optimisticData: [...products, product], rollbackOnError: true, revalidate: false } );
}
function handleFieldUpdate(e) {
const element = e.target;
const value = element.type === 'number' ? parseInt(element.value) : element.value;
const nextProduct = {...product, [element.name]: value};
setProduct(nextProduct);
setDisabled(!nextProduct.name || !nextProduct.price);
}
return(
<div className="product-form">
<input
type="text"
name="name"
placeholder="Name"
autoFocus
onChange={handleFieldUpdate}/>
<input
type="number"
name="price"
min="1"
placeholder="Price"
onChange={handleFieldUpdate}/>
<button onClick={handleAdd} disabled={disabled}>Add</button>
</div>
);
}
仔细阅读mutate
的函数调用:
mutate(async () => {
return [...products, await addProduct(product)]
}, {
optimisticData: [...products, product],
rollbackOnError: true,
revalidate: false
}
);
在这里,我们要求SWR用optimisticData
选项直接更新渲染好的产品;然后,我们可以用addProduct
函数调用将指定的元素插入数据库中。我们也可以从异步函数中返回更新的产品列表,因为我们的SWR突变期望从异步函数的返回值中得到更新的数据记录。
作为最后一步,添加导出的App
组件并完成实现:
function App() {
const [ mode, setMode ] = useState('list');
return (
<>
<div className="menu-bar">
<div onClick={() => { setMode('list') }}
className={mode === 'list' ? 'selected' : ''}>All products</div>
<div onClick={() => { setMode('add') }}
className={mode === 'add' ? 'selected' : ''}>Add product</div>
</div>
<div className="wrapper">
{ mode === 'list' ? <Products/> :
<AddProduct goToList={() => setMode('list')}/> }
</div>
</>
);
}
export default App;
现在运行该应用程序:
npm start
# --- or ---
yarn start
首先,研究SWR如何缓存ProductList
组件的数据--你将只看到一次加载文本。稍后,你将收到缓存的内容。
请看下面的预览:
用SWR缓存产品列表数据
接下来,请注意SWR是如何通过直接操作渲染的内容,然后在后台更新和重新获取AddProduct
组件中的数据来提高可用性的。添加一个新的产品,可以看到数据记录被立即渲染出来,如下图所示。
在调用API之前更新缓存的内容
最后,SWR带有一些额外的功能,比如焦点上的重新验证和检查网络标签以查看网络调用。
测试焦点上的重新验证功能
什么是TanStack Query?
TanStack Query是另一个开源的、全功能的、TypeScript就绪的库,为React应用中的数据获取和缓存提供了一个API。它在一个单独的内部包中实现了库的不可知的核心逻辑,并为React专门提供了React Query适配器包。
TanStack Query for React提供Hooks、类和一个官方的、基于GUI的专用开发者工具,用于在React应用中同步客户端状态和服务器状态。同样地,开发团队计划为其他前端库提供官方适配器包,即TanStack Vue Query、Svelte Query等。
TanStack Query于2014年首次通过其v0.0.6公开发布,大约在React首次发布一年后。
突出的特点
这个库提供了以下突出的特性:
特点 | 描述 |
---|---|
包含电池的、类似框架的体验 | TanStack Query为React开发者提供了类似框架的体验,有专门的开发者工具,每个特定任务都有专门的Hooks,OOP类可以更好地组织代码,还有基于JavaScript-props的事件处理程序。 |
详细、可配置和可重用的API | TanStack Query努力提供一个详细的、可配置的、功能齐全的API,用于在React应用程序中获取和缓存远程数据。它从其API核心提供了多个钩子和类,以便更好地组织代码。 |
为开发者和用户提供的内置功能 | TanStack Query支持分页请求,并提供useInfiniteQuery Hook来实现无限加载。 |
它还为开发者提供了React Suspense API、SSG和SSR支持--预取、焦点上的重新验证和网络状态的重新获取,像为应用程序用户提供的可用性增强。 |
|
使用TanStack查询
现在我们已经回顾了TanStack Query在React中提供的优化数据获取的功能,让我们创建一个样本应用并对其进行评估,找出与React SWR的比较点。
如果你已经是TanStack Query的用户,或者你之前已经试验过TanStack Query,你可以在这个GitHub仓库查看完整的项目,并跳到比较部分。
首先,配置模拟API服务器,并像我们在React SWR部分做的那样启动它。现在,创建另一个React项目,用TanStack Query实现之前的简单产品管理应用:
npx create-react-app tanstack-query-example
cd tanstack-query-example
安装软件包
用以下命令安装@tanstack/react-query
包:
npm install @tanstack/react-query
# --- or ---
yarn add @tanstack/react-query
安装Axios包,并按照我们在SWR部分所做的相同步骤定义基础URL。准备好用TanStack Query重写以前的应用程序!
清理App.js
文件中的所有内容,并添加以下导入:
import React, { useState } from 'react';
import {
QueryClient,
QueryClientProvider,
useQuery,
useQueryClient,
useMutation } from '@tanstack/react-query';
import axios from 'axios';
import './App.css';
在这里,useQuery
和useMutation
钩子有助于数据的获取和更新(缓存数据)。我们可以使用QueryClient
类来创建一个类似broker的实例来访问或操作缓存数据。useQueryClient
钩子返回所有应用程序组件中的当前QueryClient
参考。
QueryClientProvider
组件能够访问整个React应用的缓存数据,类似于React Context API中内置的Context.Provider
组件。
实现数据获取器和自定义Hooks
与SWR类似,现在我们可以为Axios创建一个包装器,一个向数据库插入产品的函数,以及一个获取缓存产品的自定义Hook,如下所示:
function fetcher(url) {
return axios.get(url).then(res => res.data);
}
async function addProduct(product) {
let response = await axios.post('/products', product);
return response.data;
}
function useProducts() {
const { data, isLoading, error } = useQuery(['products'], () => fetcher('/products'));
return {
products: data,
isLoading,
isError: !!error
};
}
与SWR不同的是,在这里,我们有方便的isLoading
道具来进行条件渲染,但是在第4版中,我们需要同时发送一个基于数组的唯一密钥和一个URL段给useQuery
Hook,因为Hook用一个上下文对象调用fetcher函数--它不像SWR那样直接传递唯一密钥字符串。
使用TanStack Query来获取数据
我们可以使用SWR项目中相同的Products
组件源,因为自定义的Hook几乎是一样的:
function Products() {
const { products, isLoading, isError } = useProducts();
if(isError)
return (
<div>Unable to fetch products.</div>
);
if(isLoading)
return (
<div>Loading products...</div>
);
return (
products.map((product) => (
<div key={product.id} className="product-item">
<div>{product.name}</div>
<div>${product.price}</div>
</div>
))
);
}
我们可以在多个组件中使用useProducts
Hook,而不用担心RESTful HTTP请求的重复问题,因为TanStack Query也会像SWR那样重复处理类似的请求。
突变缓存数据并使请求失效
创建一个名为AddProduct
的组件,用下面的代码实现一个添加新产品的方法:
function AddProduct({ goToList }) {
const { products } = useProducts();
const queryClient = useQueryClient();
const mutation = useMutation((product) => addProduct(product), {
onMutate: async (product) => {
await queryClient.cancelQueries(['products']);
const previousValue = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old) => [...old, product]);
return previousValue;
},
onError: (err, variables, previousValue) =>
queryClient.setQueryData(['products'], previousValue),
onSettled: () => queryClient.invalidateQueries(['products'])
});
const [product, setProduct] = useState({
id: products ? products.length + 1 : 0,
name: '',
price: null
});
const [disabled, setDisabled] = useState(true);
async function handleAdd() {
setTimeout(goToList);
mutation.mutate(product);
}
function handleFieldUpdate(e) {
const element = e.target;
const value = element.type === 'number' ? parseInt(element.value) : element.value;
const nextProduct = {...product, [element.name]: value};
setProduct(nextProduct);
setDisabled(!nextProduct.name || !nextProduct.price);
}
return(
<div className="product-form">
<input
type="text"
name="name"
placeholder="Name"
autoFocus
onChange={handleFieldUpdate}/>
<input
type="number"
name="price"
min="1"
placeholder="Price"
onChange={handleFieldUpdate}/>
<button onClick={handleAdd} disabled={disabled}>Add</button>
</div>
);
}
TanStack Query提供了一个全功能的突变API,提供对整个突变生命周期的透明访问。正如你所看到的,我们有onMutate
,onError
, 和onSettled
回调来实现我们的变异策略。
在这个例子中,我们直接用新的产品对象更新缓存数据,然后让TanStack Query向POST
端点发送请求,在后台更新服务器状态。
SWR提供的突变策略是一个内置的功能,对自定义的支持有限,但这并不是一个破坏因素,因为SWR的固定突变策略几乎解决了所有开发者的需求。然而,TanStack Query可以让你按照自己的意愿实现突变策略,这与SWR不同。
让我们为App
组件创建一个新的查询客户端:
const queryClient = new QueryClient();
一个查询客户端实例有助于提供对每个应用组件中的缓存数据记录的访问。
最后,将导出的App
组件源添加到你的App.js
文件:
function App() {
const [ mode, setMode ] = useState('list');
return (
<QueryClientProvider client={queryClient}>
<div className="menu-bar">
<div onClick={() => { setMode('list') }}
className={mode === 'list' ? 'selected' : ''}>All products</div>
<div onClick={() => { setMode('add') }}
className={mode === 'add' ? 'selected' : ''}>Add product</div>
</div>
<div className="wrapper">
{ mode === 'list' ? <Products/> :
<AddProduct goToList={() => setMode('list')}/> }
</div>
</QueryClientProvider>
);
}
export default App;
我们现在需要通过提供查询客户端引用,用QueryClientProvider
库组件包装我们的应用程序组件,以使useQueryClient
在所有子组件中正常运行。
启动RESTful模拟服务器并运行应用程序--你会看到我们用SWR实现的同样的应用程序。试着打开两个标签并添加新的产品;你会看到焦点上的重新验证功能在运行,正如我们所期望的。
现在,让我们根据上述发现来比较SWR和TanStack Query库。
SWR vs. TanStack Query
基本的CRUD功能
早些时候,我们尝试了数据检索和操作(获取和变异)来测试两个缓存库的CRUD支持。SWR和TanStack Query都提供了实现示例应用程序所需的功能。
SWR努力以最小的方式提供每一个功能,这可能促使开发者为数据缓存相关的活动写更少的代码。但最小的API设计有时会带来深入定制的限制。TanStack Query以比SWR更可定制的方式提供基本的获取和变异功能,而SWR则以比TanStack Query更简约的方式提供类似的功能。
这两个库都是与后端无关的,都有一个基于承诺的获取器功能,所以你可以将SWR和TanStack Query与REST、GraphQL或任何其他你喜欢的首选库的通信机制一起使用。Axios,Unfetch,graphql-request等。
总的来说,这两个库都应该满足开发者对基本的获取和变异支持的要求。
受欢迎程度和开发者支持
一个开源库通常会在一些情况下变得流行并获得GitHub的追星族:
- 当更多的开发者使用该特定库时
- 当该库提供了比平均水平更好的开发者支持时
- 当他们的存储库维护良好时
这两个库都有很多 GitHub 追星族。这两个库也都有很好的开发者社区--开发者通过回答这些库的GitHub仓库中的支持查询而互相帮助。React SWR不提供官方的开发者工具来进行调试,但一个社区成员创建了一个GUI开发者工具来进行调试。
与SWR相比,TanStack Query保持着更详细、更有组织、更有支持性的官方文档。然而,这两个库都提供了优秀的示例项目/代码片段,供开发者快速理解其基本概念。
开发者工具
专用的GUI调试工具对于第三方库来说不是必须的。不过,像缓存这样的主题确实很复杂,所以一个缓存库的开发者工具确实可以节省开发时间。
TanStack Query带有一个官方的开发者工具,但是SWR的开发者社区为SWR创建了一个非官方的但是很好用的swr-devtools。
swr-devtools是最小的,只向你显示只读数据,但它确实包括你调试所需的关键信息。
SWR DevTools社区GUI显示过去的查询
TanStack Query开发者工具向你显示缓存的数据,并让你操作缓存的内容,与只读的swr-devtools不同。
TanStack Query DevTools GUI显示当前的查询情况
根据GitHub问题跟踪器,swr-devtools项目正计划在开发者工具面板中加入对缓存操作的支持。
内建的可用性功能
使用SWR或TanStack Query这样的库有三个关键原因:
- 减少你为同步服务器状态和React应用状态所需编写的代码量
- 通过数据缓存和重复查询(deduplicated query)优化使用远程资源,比如概念
- 通过实时体验提高应用程序的可用性
可用性的提高是缓存和查询优化的一个重要原因,所以这两个库都竞争性地提供了以下可用性功能:
- 焦点上的重新验证
- 网络状态的重新获取
- 数据预取
- 基于时间间隔的重新验证
TanStack Query提供以下额外的可用性功能:
- 滚动恢复,当用户再次返回组件时保存无限滚动的位置
- 查询取消以停止长期运行的查询
- 支持离线突变
捆绑大小和性能优化
并非所有用户都有超快的网络连接或使用高端电脑。因此,保持一个健康的数据包大小并实施性能优化,有助于所有用户顺利运行你的应用程序,无论他们的互联网速度和计算机规格如何。对于网络应用来说,从用户的电脑中消耗最佳的硬件资源是一个好的做法。
React SWR是一个非常轻便的库。BundlePhobia测量其gzipped大小只有4.2kB。TanStack Query由于其广泛的功能而有点重,所以它的gzipped大小为11.4 kB。它确实是React核心库大小的四倍多!这就是React的核心库。
这两个库都在内部进行渲染优化、重复数据删除和缓存优化。请注意,缓存库不会提高HTTP请求的处理速度--HTTP请求的性能取决于各种因素,如HTTP客户端库的性能、浏览器的JavaScript引擎实现、网络速度、当前的CPU负载等。
SWR vs. TanStack查询:总结
让我们把上述比较因素总结在一个表格中。请看下表,将SWR和TanStack Query进行并列比较。
比较因素 | React SWR | TanStack Query |
---|---|---|
整体API设计 | 为开发者提供了一个最小的API,有一些固定的功能 | 为开发者提供了详细的、有点复杂的API,具有完全可定制的功能 |
捆绑的大小(gzipped) | 4.2 KB | 11.4 KB |
受欢迎程度、社区支持和文档 | 良好的社区,维护良好的资源库,以及整体良好的文档与演示 | 良好的社区,维护良好的存储库,以及内容丰富的文档,有许多实际的例子和完整的API参考。 |
基本的数据获取和变异功能 | 满足开发者的要求,但开发者必须为某些功能编写额外的代码,并可能面临深度定制问题 | 满足开发者的要求,并有深入的定制支持。试图将其与小型项目整合的开发者可能会发现API比它应该有的更复杂一些 |
性能优化 | 支持重复数据删除请求、渲染优化和优化缓存 | 支持重复数据删除请求、渲染优化和优化缓存 |
内置的实用性功能 | 焦点重审、网络状态重取、数据预取和基于间隔的重审 | 焦点重现、网络状态重取、数据预取、基于时间间隔的重新验证、请求取消、离线突变和滚动恢复 |
为开发者提供的内置功能 | 提供分页和无限加载功能。开发者社区实现了一个带有Chrome和Firefox扩展的开发者工具GUI。支持将缓存持久化到外部存储位置(即localStorage )。 | 提供分页和无限加载功能。它配备了一个官方的开发者工具GUI,支持缓存操作。支持将缓存持久化到外部存储位置(即localStorage )。 |
React Suspense | 支持 | 支持 |
对其他前端库的官方支持 | 没有,类似的社区库有:sswr | 正在进行中,类似的社区库有:vue-query |
总结
在这篇文章中,我们用SWR和TanStack Query库创建了一个React应用样本,然后我们根据开发者的经验和可用的功能对它们进行了比较。
这两个库都竞争性地表现得更好,并且有各种优点和缺点,我们在这里已经概述了。React SWR的目标是提供一个最小的API,通过维护一个轻量级的库来解决React请求处理中的缓存问题。同时,TanStack Query努力为同样的问题提供一个全功能的解决方案。
有时,TanStack Query看起来像一个框架,从一个开发工具包中提供你所需要的一切,就像Angular--另一方面,SWR看起来更像React,因为它只专注于解决一个问题。React引入了功能组件,以减少基于类的组件的复杂性,所以喜欢这种简单性的开发者可能会喜欢SWR而不是TanStack Query。
喜欢使用详细/稳健的API并寻求框架式、一体化的数据缓存解决方案的开发者可能会选择TanStack Query而不是SWR。TanStack Query团队正计划为Svelte、SolidJS、Vue.js和vanilla JavaScript应用提供官方支持,为核心TanStack Query库提供适配器库。然而,前端开发者社区已经根据TanStack Query和React SWR APIs为其他前端框架实现了几个开源的缓存库。
我们的结论是?试试这两个库。根据你的API偏好选择一个。TanStack Query有几个独特的功能和深入的定制,而SWR在写这篇文章时并不支持。这两个库很可能会变得同样功能齐全和强大,特别是如果SWR承诺在不久的将来实现一些缺失的功能。
然而,从最小的API设计角度来看,SWR已经很完整了,并且提供了你所寻求的强制性功能,而不需要进一步增加包的大小,TanStack Query肯定会这样做。