
아래 글은 tanstack query 공식 문서(5.80.7)를 기반으로 쓰여졌습니다. 대부분의 예제와 설명은 tanstack query 문서에서 가져왔습니다.
useQuery 가 실패하면, Tanstack Query 는 자동으로 설정된 retry 횟수만큼 재시도한다.
import { useQuery } from '@tanstack/react-query'
// Make a specific query retry a certain number of times
const result = useQuery({
queryKey: ['todos', 1],
queryFn: fetchTodoListPage,
retry: 10, // Will retry failed requests 10 times before displaying an error
})
retry = false 는 retry 를 비활성화한다.retry = 6 은 6번 재시도한다.retry = true 는 성공할 때까지 계속 재시도한다.retry = (failureCount, error) => ... 과 같이 커스텀 로직을 구현할 수 있다."서버의 경우 렌더링 속도를 올리기 위해 기본적으로
retry가 0 으로 설정되어 있다."
error프로퍼티의 내용은 마지막 재시도까지failureReason에 포함된다. 모든 재시도 이후에도 에러가 지속된다면error에 저장된다.
기본적으로 Tanstack Query 는 요청이 실패되었을 때 바로 다음 요청을 보내지않고, 백 오프를 적용한다.
retryDelay 는 1000 ms 부터 시작해 2배로 증가하여 30초를 넘기지 않는 것이 기본값이다.
// Configure for all queries
import {
QueryCache,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
추천되지는 않지만, 아래와 같이 바로 요청을 보낼 수 있다.
const result = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
})
쿼리 키에 페이지 정보를 담으면 Tanstack Query 에서 자동으로 페이징된 데이터를 받아올 수 있다.
const result = useQuery({
queryKey: ['projects', page],
queryFn: fetchProjects,
})
다만 위와 같은 코드는 각 페이지를 새로운 쿼리로 취급하기 때문에 success 와 pending 상태를 오가며 UI 가 전환된다.
placeholderDataplaceholderData 를 (previousData) => previousData 로 설정하거나 keepPreviousData 함수를 사용해서 다음과 같은 이점을 얻을 수 있다.
data 변수가 새로운 데이터로 변경된다.data 로 현재 대체되어 제공되는지 isPlaceholderData 로 제공된다.import { keepPreviousData, useQuery } from '@tanstack/react-query'
function Todos() {
const [page, setPage] = React.useState(0)
const fetchProjects = (page = 0) =>
fetch('/api/projects?page=' + page).then((res) => res.json())
const { isPending, isError, error, data, isFetching, isPlaceholderData } =
useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
placeholderData: keepPreviousData,
})
return (
<div>
{isPending ? (
<div>Loading...</div>
) : isError ? (
<div>Error: {error.message}</div>
) : (
<div>
{data.projects.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</div>
)}
<span>Current Page: {page + 1}</span>
<button
onClick={() => setPage((old) => Math.max(old - 1, 0))}
disabled={page === 0}
>
Previous Page
</button>
<button
onClick={() => {
if (!isPlaceholderData && data.hasMore) {
setPage((old) => old + 1)
}
}}
// Disable the Next Page button until we know a next page is available
disabled={isPlaceholderData || !data?.hasMore}
>
Next Page
</button>
{isFetching ? <span> Loading...</span> : null}
</div>
)
}
useInfiniteQuery 를 통해 무한 스크롤과 같은 기능을 쉽게 구현할 수 있다.
useInfiniteQuery 에서는 다음과 같은 점들이 달라진다.
data 가 무한한 쿼리 데이터를 포함하는 object 가 된다.data.pages 배열이 fetch 된 페이지를 포함한다.data.pageParams 배열이 페이지를 fetch 하기 위해 사용된 페이지 매개변수를 포함한다.fetchNextPage, fetchPreviousPage 가 활성화된다. (fetchNestPage 는 필수)initialPageParam 옵션이 활성화되고, 초기 페이지 매개변수를 명시한다. (필수)getNextPageParam 과 getPreviousPageParam 옵션들은 추가적으로 로딩할 데이터가 있는지와 해당 데이터를 fetch 할 정보를 확인하는 데 사용할 수 있다. hasNextPage 가 활성화되고 getNextPageParam 이 null 이나 undefined 가 아닌 정보를 반환한다면 true 이다.isFetchingNextPage 와 isFetchingPreviousPage 를 통해 백그라운드 새로고침 상태와 추가 로딩 상태를 구별할 수 있다.import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
지속적인 fetch 가 진행 중일 때 fetchNextPage 를 호출한다면 백그라운드에서 데이터 새로고침을 덮어쓸 수 있다. 특히 list 를 렌더링하면서 fetchNextpage 를 동시에 호출한다면 더 심각해진다.
한번의 지속적인 fetch 만 Infinite Query 에 있을 수 있다. 단일 캐시 항목이 모든 페이지에 공유되므로, 동시에 여러 번의 fetch 를 시도하는 것은 데이터 덮어쓰기의 위험이 있다.
동시 fetch 를 활성화하려면, fetchNextPage 를 { cancelRefetch: false } 로 설정해야 한다.
충돌 없이 원활한 쿼리 프로세스를 보장하려면, 쿼리가 isFetching 상태에 있는지 확인하는 것을 강하게 추천한다.
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
Infinite Query 데이터가
stale로 만료되어 다시 fetch 된다면, 처음부터 순차적으로 가져온다. 이는 중복이나 생략과 같은 현상을 방지할 수 있다.
React 의 Suspense 와 함께 다음의 훅과 같이 동작할 수 있다.
useSuspenseQueryuseSuspenseInfiniteQueryuseSuspenseQueriesuseQuery().promise & React.use() (실험적 기능)suspense 모드를 사용할 때는, status 와 error 는 React.Suspense 컴포넌트에게 대체된다.
import { useSuspenseQuery } from '@tanstack/react-query'
const { data } = useSuspenseQuery({ queryKey, queryFn })
반면에 쿼리의 활성화 유무를 조절할 수 없다. suspense 로 인해 컴포넌트 안의 쿼리는 직렬적으로 fetch 된다.
placeholderData 도 존재하지 않는다. 업데이트 도중 UI 가 fallback 상태로 변하는 것을 막으려면, startTransition 을 통해 쿼리 키를 변화시키는 작업을 감싸야 한다.
기본적으로 모든 오류가 근처의 error boundary 에 전달되지 않는다. error boundary 에게 전달되도록 하고 싶다면 명시적으로 에러를 던져야 한다.
import { useSuspenseQuery } from '@tanstack/react-query'
const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn })
if (error && !isFetching) {
throw error
}
// continue rendering data
쿼리 오류는 QueryErrorResetBoundary 컴포넌트나 useQueryErrorResetBoundary 훅을 통해 초기화할 수 있다.
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
const App = () => (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
훅을 사용한다면, 가장 가까운 QueryErrorResetBoundary 의 쿼리 오류를 초기화시킨다. 만약 아무 boundary 도 없다면, 전역적으로 초기화한다.
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
const App = () => {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
</div>
)}
>
<Page />
</ErrorBoundary>
)
}
Next.js 를 사용한다면, @tanstack/react-query-next-experimental 을 통해 클라이언트 컴포넌트에서 useSuspenseQuery 를 호출해 서버에서 데이터를 fetch 할 수 있다. SuspenseBoundaries 가 확인되면 서버에서 클라이언트로 결과가 스트리밍된다.
이 기능을 사용하기 위해서는 ReactQueryStreamedHydration 컴포넌트로 앱을 감싸야 한다.
// app/providers.tsx
'use client'
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export function Providers(props: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{props.children}
</ReactQueryStreamedHydration>
</QueryClientProvider>
)
}
추가적으로 Advanced Rendering & Hydration 과 NextJs Suspense Streaming Example 에서 더 많은 정보를 얻을 수 있다.
UseQuery().promise, React.use()이 기능을 사용하려면 experimental_prefetchInRender 옵션을 true 로 설정하여 QueryClient 를 생성해야 한다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
})
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { fetchTodos, type Todo } from './api'
function TodoList({ query }: { query: UseQueryResult<Todo[]> }) {
const data = React.use(query.promise)
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
export function App() {
const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
return (
<>
<h1>Todos</h1>
<React.Suspense fallback={<div>Loading...</div>}>
<TodoList query={query} />
</React.Suspense>
</>
)
}
병렬 쿼리 (Parallel Query) 의 양이 변하지 않으면, 그냥 나란히 useQuery 나 useInfiniteQuery 훅을 사용한다면 자동으로 적용이 된다.
function App () {
// The following queries will execute in parallel
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
...
}
React Query 를 Suspence 모드에서 사용한다면, 내부적으로 첫 번째 쿼리가 Promise 를 반환해 다른 쿼리가 실행되기 전에 컴포넌트를 유예시키기 때문에 병렬 쿼리로 작동하지 않는다.
useSuspenseQueries(권장) 나 각 컴포넌트의useSuspenseQuery인스턴스를 사용해 자체적인 병렬 처리를 구현해야 한다
useQueries 와 Dynamic Parallel Query만약 렌더링마다 실행해야 할 쿼리의 양이 바뀐다면, 훅의 규칙을 어기기 때문에 정석적인 쿼리 작업으로 실행할 수 없다. 이럴 때는 useQueries 를 통해 원하는 쿼리들을 병렬로 실행할 수 있다.
useQueries 는 쿼리 키를 가지는 쿼리 객체의 배열을 받아 쿼리 결과의 배열을 반환한다.
function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
}),
})
}
쿼리가 자동으로 실행되는 것을 막기 위해서는, enabled = false 옵션을 사용할 수 있다. enabled 옵션은 boolean 을 반환하는 콜백도 받는다.
enabled 가 false 일 때:
status ==='success' 나 isSuccess 상태로 초기화된다.status === 'pending' 과 fetchStatus === 'idle' 상태로 초기화된다.invalidateQueries 와 refetchQueries 를 무시한다.useQuery 에서 반환되는 refetch 가 쿼리를 fetch 를 유발하도록 사용될 수 있다. 하지만 skipToken 과 작동하지 않는다.TypeScript 개발자들은
skipToken을enabled=false의 대체제로 사용하는 것을 선호할 수도 있다
function Todos() {
const { isLoading, isError, data, error, refetch, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
enabled: false,
})
return (
<div>
<button onClick={() => refetch()}>Fetch Todos</button>
{data ? (
<>
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
) : isError ? (
<span>Error: {error.message}</span>
) : isLoading ? (
<span>Loading...</span>
) : (
<span>Not ready ...</span>
)}
<div>{isFetching ? 'Fetching...' : null}</div>
</div>
)
}
쿼리를 영구적으로 비활성화하는 것은 Tanstack Query 의 많은 기능을 비활성화시키며, 선언적 접근에서 명령형 접근으로 바꾼다. 또한 refetch 로 매개변수를 전달할 수 없다. 대부분의 경우, 초기 fetch 를 지연시키는 lazy 쿼리만 필요하다.
영구적인 비활성화가 아닌, 나중을 위해 활성화/비활성화 할 수도 있다.
function Todos() {
const [filter, setFilter] = React.useState('')
const { data } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
// ⬇️ disabled as long as the filter is empty
enabled: !!filter,
})
return (
<div>
// 🚀 applying the filter will enable and execute the query
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data} />}
</div>
)
}
Lazy 쿼리는 status: 'pending' 으로 시작한다. 왜냐하면 pending 은 아직 아무런 데이터가 없다는 것을 의미하기 때문이다. 하지만, 아직 우리가 아무 데이터도 fetch 하고 있지 않기 때문에 pending 을 통해 로딩 스피너를 보여줄 수 없다는 것을 의미한다.
Disabled / Lazy 쿼리를 사용한다면, isLoading 을 대신 사용할 수 있다. 이는 isPending && isFetching 으로 계산된다. 그러므로 지금 처음 fetching 을 한다면 true 이다.
skipToken 을 통한 쿼리 비활성화TypeScript 를 사용한다면, skipToken 으로 쿼리를 비활성화시킬 수 있다. 이는 조건 별로 쿼리를 비활성화하기를 원하지만 쿼리가 type-safe 하기를 원할 때 유용하다.
useQuery 의 refetch 는 skipToken 과 동작하지 않는다. 그외에는 enabled: false 와 동일하게 동작한다.
import { skipToken, useQuery } from '@tanstack/react-query'
function Todos() {
const [filter, setFilter] = React.useState<string | undefined>()
const { data } = useQuery({
queryKey: ['todos', filter],
// ⬇️ disabled as long as the filter is undefined or empty
queryFn: filter ? () => fetchTodos(filter) : skipToken,
})
return (
<div>
// 🚀 applying the filter will enable and execute the query
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data} />}
</div>
)
}
공식 문서에서 아래의 내용을 포함하고 있는 글입니다.