JotaiJotai

상태
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-core를 설치해야 이 확장 기능을 사용할 수 있습니다.

npm install jotai-tanstack-query @tanstack/query-core

점진적 도입

jotai-tanstack-query를 여러분의 앱에 점진적으로 도입할 수 있습니다. 이는 전부 아니면 전무(all or nothing) 방식이 아닙니다. 단지 동일한 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>Loading...</div>
if (isError) return <div>Error</div>
return <div>{JSON.stringify(data)}</div>
}

atomWithInfiniteQuery 사용법

atomWithInfiniteQueryatomWithQuery와 매우 유사하지만, 페이지네이션을 위한 InfiniteQuery를 사용합니다. Infinite Queries에 대해 더 알아보기.

기존 데이터에 추가적으로 데이터를 "더 불러오기" 또는 "무한 스크롤"을 구현하는 리스트 렌더링은 매우 일반적인 UI 패턴입니다. React Query는 이러한 유형의 리스트를 쿼리하기 위해 useInfiniteQuery라는 유용한 버전의 useQuery를 지원합니다.

일반적인 쿼리 아톰과의 주요 차이점은 추가 옵션인 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>Loading...</div>
if (isError) return <div>Error</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()}>Next</button>
</>
)
}

atomWithMutation 사용법

atomWithMutation은 TanStack Query의 표준 Mutation을 구현하는 새로운 아톰을 생성합니다.

쿼리와 달리, 뮤테이션은 일반적으로 데이터를 생성/수정/삭제하거나 서버 사이드 이펙트를 수행하는 데 사용됩니다.

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' })}>Click me</button>
<pre>{JSON.stringify(status, null, 2)}</pre>
</div>
)
}

atomWithMutationState 사용법

atomWithMutationStateMutationCache에 있는 모든 뮤테이션에 접근할 수 있는 새로운 아톰을 생성합니다.

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()}>Next</button>
</>
)
}

프로젝트에서 동일한 Query Client 인스턴스 참조하기

프로젝트에서 useQueryClient() 훅을 사용하여 QueryClient 객체를 가져오고 해당 메서드를 호출하는 커스텀 훅이 있을 수 있습니다.

동일한 QueryClient 객체를 참조하려면 프로젝트의 루트를 <Provider>로 감싸고, QueryClientProvider에 제공한 것과 동일한 queryClient 값으로 queryClientAtom을 초기화해야 합니다.

이 단계를 생략하면 useQueryAtomuseQueryClient() 훅을 사용하여 queryClient를 가져오는 다른 훅들과 별개의 QueryClient를 참조하게 됩니다.

또는 getQueryClient 매개변수를 사용하여 queryClient를 지정할 수도 있습니다.

예제

아래 예제에서는 useTodoMutation 뮤테이션 훅과 todosAtom 쿼리를 사용합니다.

루트 <App> 노드에서 초기화 단계를 포함했습니다.

동일한 쿼리 키('todos')를 참조하지만, Provider 초기화 단계가 완료되지 않았다면 useTodoMutationonSuccess 무효화가 트리거되지 않습니다.

이로 인해 todosAtom은 리페치를 요청받지 않아 오래된 데이터를 표시하게 됩니다.

⚠️ 참고: Typescript를 사용할 때는 queryClient 값을 useHydrateAtoms에 전달할 때 Map을 사용하는 것이 좋습니다. Initializing State on Render 문서에서 작동 예제를 확인할 수 있습니다.

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 앱과 같은 서버 사이드 렌더링(SSR) 앱의 컨텍스트 내에서 사용할 수 있습니다. React Query가 SSR 앱 내에서 사용하기 위해 지원하는 두 가지 옵션, hydration 또는 initialData를 모두 사용할 수 있습니다.

에러 처리

Fetch 에러가 발생하면 ErrorBoundary로 잡을 수 있습니다. 일시적인 오류는 재요청을 통해 복구할 수 있습니다.

자세한 내용은 실행 가능한 예제를 참고하세요.

Devtools

Devtools를 사용하려면 추가로 설치해야 합니다.

npm install @tanstack/react-query-devtools

<QueryClientProvider /> 안에 <ReactQueryDevtools />를 넣기만 하면 됩니다.

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>
)
}

Migrate to v0.8.0

atom 시그니처 변경

모든 atom 시그니처가 TanStack Query와 더 일관되도록 변경되었습니다. v0.8.0에서는 튜플 형태의 atom 대신 단일 atom만 반환하므로, atomsWithSomething에서 atomWithSomething으로 이름이 변경되었습니다.

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

단순화된 반환 구조

이전 버전의 jotai-tanstack-query에서는 쿼리 아톰인 atomsWithQueryatomsWithInfiniteQuery[dataAtom, statusAtom] 형태의 튜플을 반환했습니다. 이 설계는 데이터와 그 상태를 두 개의 다른 아톰으로 분리했습니다.

atomWithQuery와 atomWithInfiniteQuery

  • dataAtom은 실제 데이터(TData)에 접근하는 데 사용되었습니다.
  • statusAtomisPending, isError 등의 추가 속성을 포함한 상태 객체(QueryObserverResult<TData, TError>)를 제공했습니다.

v0.8.0에서는 이들이 atomWithQueryatomWithInfiniteQuery로 대체되어 단일 dataAtom만 반환하도록 변경되었습니다. 이제 dataAtomQueryObserverResult<TData, TError>를 직접 제공하며, 이는 Tanstack Query의 바인딩과 더욱 일치하도록 조정되었습니다.

새 버전으로 마이그레이션하려면, 기존의 dataAtomstatusAtom을 분리해서 사용하던 방식을 통합된 dataAtom으로 대체해야 합니다. 이제 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에서 뮤테이션 상태에 접근하고, mutate()로 뮤테이션 실행
+ const postAtom = atomWithMutation(/* ... */);
+ const [{ data, error, mutate }] = useAtom(postAtom); // 동일한 아톰에서 뮤테이션 결과와 mutate 메서드에 접근

Examples

기본 예제

개발자 도구 데모

Hackernews