JotaiJotai

Jotai
React 原始而灵活的状态管理

Query

TanStack Query 提供了一组用于管理异步状态(通常是外部数据)的函数。

概述文档中:

React Query 常被描述为 React 缺失的数据获取库,但更技术的说法是,它使得在你的 React 应用中获取、缓存、同步和更新服务器状态变得轻而易举。

jotai-tanstack-query 是 TanStack Query 的 Jotai 扩展库。它提供了一个与所有 TanStack Query 功能的美妙接口,使你能够将这些功能与你现有的 Jotai 状态结合使用。

支持

jotai-tanstack-query 目前支持 TanStack Query v5。

安装

除了 jotai,你还需要安装 jotai-tanstack-query@tanstack/query-corewonka 来使用这个扩展。

npm i jotai-tanstack-query @tanstack/query-core wonka

渐进式采用

你可以在你的应用中渐进式地采用 jotai-tanstack-query。它不是全有或全无的解决方案。你只需要确保你正在使用相同的 QueryClient 实例。QueryClient 设置

// 现有的 useQueryHook
const { data, isPending, isError } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList
});
// jotai-tanstack-query
const todosAtom = atomWithQuery(() => ({
queryKey: ['todos'],
}))
const [{ data, isPending, isError }] = useAtom(todosAtom)

导出的函数

所有函数遵循相同的签名。

const dataAtom = atomWithSomething(getOptions, getQueryClient)

第一个 getOptions 参数是一个返回观察者输入的函数。 第二个可选的 getQueryClient 参数是一个返回 QueryClient 的函数。

atomWithQuery 的使用

atomWithQuery 创建了一个新的原子,它实现了来自 TanStack Query 的标准 Query

import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
}))
const UserData = () => {
const [{ data, isPending, isError }] = useAtom(userAtom)
if (isPending) return <div>加载中...</div>
if (isError) return <div>错误</div>
return <div>{JSON.stringify(data)}</div>
}

atomWithInfiniteQuery 的使用

atomWithInfiniteQueryatomWithQuery 非常相似,但它是用于 InfiniteQuery,这是用于分页数据的。你可以在这里阅读更多关于无限查询的内容

渲染可以增量 "加载更多" 数据到现有数据集或 "无限滚动" 的列表也是一个非常常见的 UI 模式。React Query 支持一个有用的 useQuery 版本,叫做 useInfiniteQuery,用于查询这种类型的列表。

一个标准查询原子的显著区别是额外的选项 getNextPageParamgetPreviousPageParam,这是你将用来指导查询如何获取任何额外的页面。

import { atom, useAtom } from 'jotai'
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'
const postsAtom = atomWithInfiniteQuery(() => ({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
initialPageParam: 1,
}))
const Posts = () => {
const [{ data, fetchNextPage, isPending, isError, isFetching }] =
useAtom(postsAtom)
if (isPending) return <div>加载中...</div>
if (isError) return <div>错误</div>
return (
<>
{data.pages.map((page, index) => (
<div key={index}>
{page.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button onClick={() => fetchNextPage()}>下一页</button>
</>
)
}
### atomWithMutation 的使用 {#atomwithmutation-usage}
`atomWithMutation` 创建了一个新的原子,它实现了来自 TanStack Query 的标准 [`Mutation`](https://tanstack.com/query/v5/docs/guides/mutations)
> 与查询不同,mutations 通常用于创建/更新/删除数据或执行服务器端效果。
```tsx
const postAtom = atomWithMutation(() => ({
mutationKey: ['posts'],
mutationFn: async ({ title }: { title: string }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
method: 'POST',
body: JSON.stringify({
title,
body: 'body',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const data = await res.json()
return data
},
}))
const Posts = () => {
const [{ mutate, status }] = useAtom(postAtom)
return (
<div>
<button onClick={() => mutate({ title: 'foo' })}>点击我</button>
<pre>{JSON.stringify(status, null, 2)}</pre>
</div>
)
}

atomWithMutationState 的使用

atomWithMutationState 创建了一个新的原子,它可以让你访问 MutationCache 中的所有 mutations。

const mutationStateAtom = atomWithMutationState((get) => ({
filters: {
mutationKey: ['posts'],
},
}))

Suspense

jotai-tanstack-query 也可以与 React 的 Suspense 一起使用。

atomWithSuspenseQuery 的使用

import { atom, useAtom } from 'jotai'
import { atomWithSuspenseQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const userAtom = atomWithSuspenseQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
}))
const UserData = () => {
const [{ data }] = useAtom(userAtom)
return <div>{JSON.stringify(data)}</div>
}

atomWithSuspenseInfiniteQuery 的使用

import { atom, useAtom } from 'jotai'
import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query'
const postsAtom = atomWithSuspenseInfiniteQuery(() => ({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
initialPageParam: 1,
}))
const Posts = () => {
const [{ data, fetchNextPage, isPending, isError, isFetching }] =
useAtom(postsAtom)
return (
<>
{data.pages.map((page, index) => (
<div key={index}>
{page.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button onClick={() => fetchNextPage()}>下一页</button>
</>
)
}

在你的项目中引用相同的 Query Client 实例

也许你的项目中有一些自定义的钩子,它们使用 useQueryClient() 钩子来获取 QueryClient 对象并调用其方法。

为了确保你引用的是相同的 QueryClient 对象,请确保在你的项目的根部包裹一个 <Provider>,并用你提供给 QueryClientProvider 的相同的 queryClient 值初始化 queryClientAtom

如果没有这一步,useQueryAtom 将引用与任何使用 useQueryClient() 钩子获取 queryClient 的钩子不同的 QueryClient

或者,你可以用 getQueryClient 参数指定你的 queryClient

示例

在下面的示例中,我们有一个 mutation 钩子 useTodoMutation 和一个查询 todosAtom

我们在根 <App> 节点中包含了一个初始化步骤。

尽管它们引用了相同的查询键('todos'),但如果没有进行 Provider 初始化步骤,useTodoMutation 中的 onSuccess 无效化将不会触发。

这将导致 todosAtom 显示过时的数据,因为它没有被提示重新获取。

import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import {
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient()
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
{/*
这个 Provider 初始化步骤是必需的,以便我们在 atomWithQuery 和应用程序的其他部分都引用相同的
queryClient。如果没有这个步骤,我们的 useQueryClient() 钩子将返回一个不同的 QueryClient 对象
*/}
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
</QueryClientProvider>
)
}
export const todosAtom = atomWithQuery((get) => {
return {
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
}
})
export const useTodoMutation = () => {
const queryClient = useQueryClient()
return useMutation(
async (body: todo) => {
await fetch('/todo', { Method: 'POST', Body: body })
},
{
onSuccess: () => {
void queryClient.invalidateQueries(['todos'])
},
onError,
}
)
}

服务器端渲染(SSR)支持

所有的原子都可以在服务器端渲染的应用中使用,例如 next.js 应用或 Gatsby 应用。你可以使用 React Query 支持的两种选项,hydrationinitialData

错误处理

获取错误将会被抛出,并且可以被 ErrorBoundary 捕获。 重新获取可能会从临时错误中恢复。

查看工作示例以了解更多。

开发者工具

为了使用开发者工具,你需要额外安装它。

npm i @tanstack/react-query-devtools

你需要做的就是将 <ReactQueryDevtools /> 放在 <QueryClientProvider /> 内。

import {
QueryClientProvider,
QueryClient,
QueryCache,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
<ReactQueryDevtools />
</QueryClientProvider>
)
}

迁移到 v0.8.0

原子签名的变化

所有的原子签名都已经改变,以与 TanStack Query 更加一致。 v0.8.0 只返回一个原子,而不是一对原子,因此从 atomsWithSomething 改名为 atomWithSomething

- const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient)
+ const dataAtom = atomWithSomething(getOptions, getQueryClient)

简化的返回结构

jotai-tanstack-query 的前一个版本中,查询原子 atomsWithQueryatomsWithInfiniteQuery 返回了一对原子:[dataAtom, statusAtom]。这种设计将数据及其状态分成了两个不同的原子。

atomWithQuery 和 atomWithInfiniteQuery

  • dataAtom 被用来访问实际的数据 (TData)。
  • statusAtom 提供了状态对象 (QueryObserverResult<TData, TError>),其中包括了额外的属性,如 isPendingisError 等。

在 v0.8.0 中,它们被 atomWithQueryatomWithInfiniteQuery 替代,只返回一个 dataAtom。这个 dataAtom 现在直接提供了 QueryObserverResult<TData, TError>,使其与 Tanstack Query 的绑定行为更加接近。

要迁移到新版本,将分开的 dataAtomstatusAtom 的使用替换为现在包含数据和状态信息的统一的 dataAtom

- const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
- const [data] = useAtom(dataAtom);
- const [status] = useAtom(statusAtom);
+ const dataAtom = atomWithQuery(/* ... */);
+ const [{ data, isPending, isError }] = useAtom(dataAtom);

atomWithMutation

atomsWithQueryatomsWithInfiniteQuery 类似,atomWithMutation 也返回一个原子而不是一对原子。原子值的返回类型是 MutationObserverResult<TData, TError, TVariables, TContext>

- const [, postAtom] = atomsWithMutation(/* ... */);
- const [post, mutate] = useAtom(postAtom); // 从 post 访问 mutation 状态;并使用 mutate() 执行 mutation
+ const postAtom = atomWithMutation(/* ... */);
+ const [{ data, error, mutate }] = useAtom(postAtom); // 从同一个原子访问 mutation 结果和 mutate 方法

示例

基础演示

开发者工具演示

黑客新闻