React Query는 React 애플리케이션에서 데이터를 관리하고 API 요청을 처리하는 데 사용되는 라이브러리입니다. Tanstack Query는 이 라이브러리의 이전 버전인 React Query의 일부분입니다. React Query는 데이터 캐시, API 요청 및 데이터 동기화를 단순화하고 최적화하기 위해 설계되었습니다.
사용 이유:
간편한 데이터 캐시: React Query는 API 요청의 결과를 캐시하여 성능을 최적화합니다. 데이터를 캐시하고 자동으로 최신 상태를 유지하여 UI의 데이터를 업데이트할 때 불필요한 네트워크 요청을 줄일 수 있습니다.
오프라인 지원: React Query는 로컬 캐시를 사용하여 오프라인 상태에서도 데이터를 제공하고 처리할 수 있습니다. 이는 네트워크 연결이 끊어진 경우에도 애플리케이션의 성능을 유지하는 데 도움이 됩니다.
API 요청 관리: React Query는 API 요청을 간편하게 처리하고 관리할 수 있습니다. RESTful API, GraphQL, WebSockets 등 다양한 유형의 API와 통합할 수 있습니다.
자동화: React Query는 데이터를 가져오고 캐시하며 오류를 처리하는 데 필요한 많은 작업을 자동화합니다. 이를 통해 개발자는 더욱 효율적으로 코드를 작성하고 관리할 수 있습니다.
쉬운 사용성: React Query는 간단하고 직관적인 API를 제공하여 쉽게 사용할 수 있습니다. React 애플리케이션에서 빠르게 통합하고 사용할 수 있습니다.
React Query는 React 애플리케이션에서 데이터 관리를 단순화하고 성능을 최적화하는 데 매우 유용한 도구입니다. 이를 통해 개발자는 더욱 빠르고 효율적으로 애플리케이션을 개발할 수 있습니다.
기본적으로 TanStack 쿼리는 공격적이지만 정상적인 기본값으로 구성됩니다. 때때로 이러한 기본값은 새로운 사용자를 당황하게 만들거나 사용자가 이를 모르는 경우 학습/디버깅을 어렵게 만들 수 있습니다. TanStack 쿼리를 계속 배우고 사용하면서 다음 사항을 염두에 두십시오.
useQuery
또는 useInfiniteQuery
를 통한 쿼리 인스턴스는 기본적으로 캐시된 데이터를 오래된 것으로 간주합니다.
이 동작을 변경하려면 staleTime
옵션을 사용하여 전역적으로 쿼리별로 쿼리를 구성할 수 있습니다. staleTime
을 더 길게 지정하면 쿼리가 데이터를 자주 다시 가져오지 않음을 의미합니다.
다음과 같은 경우 오래된 쿼리가 백그라운드에서 자동으로 다시 가져옵니다. :
이 기능을 변경하려면 refetchOnMount
, refetchOnWindowFocus
, refetchOnReconnect
및 refetchInterval
과 같은 옵션을 사용할 수 있습니다.
useQuery
, useInfiniteQuery
또는 쿼리 관찰자의 활성 인스턴스가 더 이상 없는 쿼리 결과는 "비활성"으로 표시되고 나중에 다시 사용될 경우를 대비해 캐시에 남아 있습니다.
기본적으로 "비활성" 쿼리는 5분 후에 Garbage Collect 되어 사라집니다.
이를 변경하려면 쿼리의 기본 gcTime
을 1000 * 60 * 5
ms(5분)가 아닌 다른 값으로 변경할 수 있습니다.
쿼리가 실패됐을 경우 오류는 캡쳐되고 UI에 표시하기 전에 기하급수적인 백오프 지연하여 자동으로 3번 다시 시도됩니다.
이를 변경하려면 쿼리에 대한 기본 retry
및 retryDelay
옵션을 3이 아닌 다른 값과 기본 지수 백오프 기능으로 변경할 수 있습니다.
기본적으로 쿼리 결과는 데이터가 실제로 변경되었는지 감지하기 위해 구조적으로 공유되며, 그렇지 않은 경우 useMemo
및 useCallback
과 관련된 값 안정화에 더 나은 도움을 주기 위해 데이터 참조는 변경되지 않은 상태로 유지됩니다. 이 개념이 낯설게 들린다면 걱정하지 마세요! 99.9%의 경우 이 기능을 비활성화할 필요가 없으며 이를 통해 무료로 앱 성능을 높일 수 있습니다.
구조적 공유는 JSON 호환 값에서만 작동하며 다른 값 유형은 항상 변경된 것으로 간주됩니다. 예를 들어 큰 응답으로 인해 성능 문제가 발생하는 경우 config.structuralSharing
플래그를 사용하여 이 기능을 비활성화할 수 있습니다. 쿼리 응답에서 JSON과 호환되지 않는 값을 처리하고 데이터가 변경되었는지 여부를 계속 감지하려는 경우 config.structuralSharing
으로 고유한 사용자 지정 함수를 제공하여 참조를 유지하면서 이전 응답과 새 응답에서 값을 계산할 수 있습니다. 필요에 따라.
React Query를 사용한 간단한 예제를 제공하겠습니다. 이 예제에서는 공공 API인 JSONPlaceholder에서 포스트 데이터를 가져와서 표시하는 React 애플리케이션을 만들어 보겠습니다.
먼저, React Query를 설치합니다.
npm i @tanstack/react-query
이제 예제 코드를 작성해 보겠습니다.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client 클라이언트 생성
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App 프로바이더 추가
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client 클라이언트 접근
const queryClient = useQueryClient()
// Queries 쿼리
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations 변화
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch 무효화 & 재패치
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
이 코드는 Tanstack의 React Query를 사용하여 간단한 To-Do 애플리케이션을 만드는 예제입니다. 이 코드에서는 useQuery
, useMutation
, useQueryClient
, QueryClient
, QueryClientProvider
등을 사용하여 React Query의 다양한 기능을 활용하고 있습니다.
주요 기능은 다음과 같습니다:
QueryClient 생성: const queryClient = new QueryClient()
를 사용하여 React Query의 클라이언트를 생성합니다.
App 컴포넌트 내에서 QueryClientProvider 제공: QueryClientProvider
를 사용하여 애플리케이션의 루트 컴포넌트인 App
에 클라이언트를 제공합니다.
Todos 컴포넌트 내에서 QueryClient 사용: useQueryClient
훅을 사용하여 클라이언트에 접근합니다.
useQuery 훅을 통한 데이터 가져오기: useQuery
훅을 사용하여 getTodos
함수를 호출하여 To-Do 목록을 가져옵니다.
useMutation 훅을 통한 데이터 변경: useMutation
훅을 사용하여 postTodo
함수를 호출하여 새로운 To-Do를 추가합니다. 성공적으로 To-Do가 추가되면 onSuccess
콜백에서 쿼리를 무효화하고 다시 가져옵니다.
To-Do 목록 및 추가 버튼 렌더링: To-Do 목록과 추가 버튼을 렌더링합니다. 버튼을 클릭하면 mutation.mutate
를 사용하여 새로운 To-Do를 추가합니다.
이 코드는 Tanstack의 React Query를 사용하여 간단한 상태 관리 및 비동기 데이터 처리를 보여주는 예제입니다.
useQuery
를 사용해 컴포넌트나 커스텀 훅에 있는 쿼리를 불러온다.
useQuery
는 최소한 필요한 것들이 있는 데,
1. unique key: 쿼리를 가져오고, 캐싱하고, 공유하는 데 사용하는 것.
2. 에러 처리: throw new Error()
3. 함수
이 모두를 만족해야 합니다.
예시:
import { useQuery } from '@tanstack/react-query'
function App() {
const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
useQuery
에서 반환 되는 것들:
1. isPending
: true 일 경우 요청을 했으나 완료를 하지 못한 경우
2. isSuccess
: true 일 경우 완료를 하였고 성공한 경우
3. isError
: true 일 경우 완료를 하였지만 에러를 받아 왔을 경우
4. status
: 요청을 하고 난 뒤 상태 반환 됨. 주 값은 'pending', 'error', 'success'
5. fetchStatus
: 주로 'fetching', 'paused', 'idle'
6. data
: 성공했을 경우 받아온 값
7. error
: 성공하지 못할 경우 받아온 에러
status
와 fetchStatus
의 다른 점:
success
in status
일 경우 idle
in fetchStatus
이 될 수 있지만 백그라운드에서 refetch하는 경우 fetching
일 상태 일 수 있다.pending
status
와 fetching
fetchStatus
될 수 있지만, 네트워크 연결이 없을 경우에 paused
가 될 수 있다.status
는 data
에 대한 상태: 값을 가지고 있나?fetchStatus
는 queryFunction
에 대한 상태: 돌아가고 있는 거 맞나?가장 간단한 형태는 상수인 배열이다.
주로 일반 리스트, 인덱스 리소스, 비계층적인 리소스에 사용된다.
예시:
// A list of todos
useQuery({ queryKey: ['todos'], ... })
// Something else, whatever!
useQuery({ queryKey: ['something', 'special'], ... })
고유한 키를 표현하기 위해 더 많은 정보가 필요한 경우 문자열과 직렬화된 개체가 포함된 배열을 사용한다.
// An individual todo
useQuery({ queryKey: ['todo', 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})
// A list of todos that are "done"
useQuery({ queryKey: ['todos', { type: 'done' }], ... })
쿼리 키는 결정적으로 해시된다. 객체 안의 아이템의 순서가 바뀌어도 동일하게 인식된다.
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
근데 배열일 경우에는 순서가 중요하다.
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})
만약에 변수가 필요한 경우 쿼리 키에 포함해 사용할 수 있다.
function Todos({ todoId }) {
const result = useQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodoById(todoId),
})
}
쿼리 함수는 무슨 함수든 간에 항상 promise
를 반환한다.
promise
는 데이터를 처리하거나 에러를 던져야한다.
사용가능한 쿼리 예제:
useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) })
useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
const data = await fetchTodoById(todoId)
return data
},
})
useQuery({
queryKey: ['todos', todoId],
queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
})
에러 핸들링 예제:
const { error } = useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
if (somethingGoesWrong) {
throw new Error('Oh no!')
}
if (somethingElseGoesWrong) {
return Promise.reject(new Error('Oh no!'))
}
return data
},
})
쿼리 인자 전달 예제:
function Todos({ status, page }) {
const result = useQuery({
queryKey: ['todos', { status, page }],
queryFn: fetchTodoList,
})
}
// Access the key, status and page variables in your query function!
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey
return new Promise()
}
queryKey
와 queryFn
을 여러 장소 간에 공유하면서도 서로의 위치를 동일하게 유지하는 가장 좋은 방법 중 하나는queryOptions
를 사용하는 것입니다. 런타임에 이 도우미는 전달한 내용을 모두 반환합니다.
예제:
import { queryOptions } from '@tanstack/react-query'
function groupOptions(id) {
return queryOptions({
queryKey: ['groups', id],
queryFn: () => fetchGroups(id),
staleTime: 5 * 1000,
})
}
// usage:
useQuery(groupOptions(1))
useSuspenseQuery(groupOptions(5))
useQueries({
queries: [groupOptions(1), groupOptions(2)],
})
queryClient.prefetchQuery(groupOptions(23))
queryClient.setQueryData(groupOptions(42).queryKey, newGroups)
병렬 쿼리를 사용하는 방법은 아주 간단하다.
하나의 컴포넌트/커스텀 훅에 여러 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 })
...
}
하지만, 실행해야 하는 쿼리 수가 렌더링마다 변경되는 경우 훅 규칙을 위반하므로 수동 쿼리를 사용할 수 없다.
동적으로 병렬 쿼리를 사용하기 위해서는 useQueries
를 사용해 원하는 만큼 여러 쿼리를 사용할 수 있다.
예제:
function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
}),
})
}
만약 이렇게 된 쿼리가 있을 경우,
projects
는 userId
에 종속되어 undefined를 가져왔을 경우에도 그대로 쿼리를 실행할 것이다.
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
// enabled: !!userId,
})
이를 예방하기 위해서 useQuery
에 enabled 속성을 설정해 userId가 undefined 일 경우에는 쿼리를 실행하지 않을 것이다.
useQueries
도 마찬가지이다.
// Get the users ids
const { data: userIds } = useQuery({
queryKey: ['users'],
queryFn: getUsersData,
select: (users) => users.map((user) => user.id),
})
// Then get the users messages
const usersMessages = useQueries({
queries: userIds
? userIds.map((id) => {
return {
queryKey: ['messages', id],
queryFn: () => getMessagesByUsers(id),
}
})
: [], // if users is undefined, an empty array will be returned
})
쿼리에서 status === 'pending'
은 초기 하드 로딩 상태를 보여줄 수 있지만, 중간에 쿼리가 백그라운드에서 다시 페칭이라는 표시를 할 수 있다. 이를 위해 isFetching
이라는 불리언도 제공한다.
function Todos() {
const {
status,
data: todos,
error,
isFetching,
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return status === 'pending' ? (
<span>Loading...</span>
) : status === 'error' ? (
<span>Error: {error.message}</span>
) : (
<>
{isFetching ? <div>Refreshing...</div> : null}
<div>
{todos.map((todo) => (
<Todo todo={todo} />
))}
</div>
</>
)
}
만약에 해당 컴포넌트 말고 다른 곳에서 페칭 중이라는 것을 표시하고 싶다면 useIsFetching
훅을 사용하면 된다.
import { useIsFetching } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
const isFetching = useIsFetching()
return isFetching ? (
<div>Queries are fetching in the background...</div>
) : null
}
창을 떠나려고 하거나 쿼리 값이 탁한 상태가 될 때, TanStack Query는 자동으로 데이터를 새로 요청할 것이다. 원치 않을 경우 refetchOnWindowFocus
옵션을 사용해 글로벌로, 혹은 쿼리당 끌 수 있다.
글로벌로 끄기
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // default: true
},
},
})
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
쿼리당 끄기
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: false,
})
다시 데이터를 검증하기 위해 TanStack Query를 트리거하는 자체 창 맞춤 이벤트를 관리할 수 있다.
이를 위해 focusManager.setEventListener
를 사용해 창이 마춰질 때 원하는 함수를 설정할 수 있다.
focusManager.setEventListener
를 사용할 경우 기존 핸들러는 제거되고 새로운 핸들러가 사용됩니다.
예제:
focusManager.setEventListener((handleFocus) => {
// Listen to visibilitychange
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('visibilitychange', () => handleFocus(), false)
return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('visibilitychange', () => handleFocus())
}
}
})
리액트 네이티브에서 창 맞춤을 관리하기 위해서는 window
대신에 AppState
를 사용합니다.
AppState
를 사용해 앱 상태가 "active"될 때 트리거를 업데이트 할 수 있다.
import { AppState } from 'react-native'
import { focusManager } from '@tanstack/react-query'
function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== 'web') {
focusManager.setFocused(status === 'active')
}
}
useEffect(() => {
const subscription = AppState.addEventListener('change', onAppStateChange)
return () => subscription.remove()
}, [])
맞춤 상태 관리하는 법:
import { focusManager } from '@tanstack/react-query'
// Override the default focus state
focusManager.setFocused(true)
// Fallback to the default focus check
focusManager.setFocused(undefined)
enabled
옵션은 껐다 키는 용도로 사용되는 데 예시로 사용자가 필터 값을 입력한 뒤에만 처음 요청을 할 수 있게 만들 수 있게 할 수 있습니다.
예제는 아래와 같습니다:
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>
)
}
아니면 상태에 따라서 쿼리 함수의 실행을 막을 수 있습니다.
function Todos() {
const [filter, setFilter] = React.useState()
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>
)
}
useQuery
를 사용할 경우 자동으로 다시 시도됩니다.
그러나 기본으로 3번까지만 시도 되고 더 이상 다시 시도 되지 않습니다.
만약 이를 바꾸거나 다시 시도하는 것을 막고 싶다면 retry
를 사용할 수 있습니다.
retry = false
는 다시 시도하지 않겠다는 의미입니다.retry = 6
는 될 때까지 6번 시도 하겠다는 의미입니다.retry = true
는 될 때까지 멈추지 않고 다시 시도 하겠다는 의미입니다.retry = failureCount, error) => ...
는 요청이 실패한 경우 따른 커스텀 로직을 허용한다는 의미입니다.예제는 다음과 같습니다:
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
})
재시도하는 지연 시간을 설정하기 원한다면, retryDelay
를 사용할 수 있습니다.
기본적으로 1000ms로 시작하여 두 배씩 주기가 길어지지만, 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>
}
추천하진 않지만 Provider
와 useQuery
에서 retryDelay
함수를 재정의 할 수 있습니다.
함수 대신 정수로 설정하면 지연 시간은 항상 동일합니다.
const result = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
})
쿼리에 대한 pageIndex를 증가시키려고 할 때 useQuery
를 사용한다면 기술적으로는 괜찮지만, UI는 각 페이지 또는 pageIndex에 대한 쿼리가 생성되고 사라지기 때문에success
와 pending
상태를 들낙한다. 자리 표시자 data를 (previousData) => previousData 함수로 유지하면 새로운 기능을 만들 수 있다.
isPlaceholderData
를 사용하면 쿼리가 현재 제공하고 있는 데이터를 알 수 있다.import { keepPreviousData, useQuery } from '@tanstack/react-query'
import React from 'react'
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
를 사용할 수 있다.
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
이렇게 계속해서 페칭을 해야 할 경우 아래와 같이 요청할 수 있다.
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 || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
양방향 통신일 경우:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
거꾸로 페이지를 표시해야 할 경우에는 select
옵션을 사용할 수 있다.
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
제한을 하고 싶을 경우에는:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
커서를 반환하지 않을 경우에는:
return useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
})
요청에 대한 응답이 오기 전에 값은 비어 있다.
비어있는 것을 막기 위해서 보통 isLoading
을 사용해서 값이 받아올 때 까지 화면을 보이지 않게 된다.
하지만 값을 받지 못하더라도 useQuery
의 initialData
를 사용해 초기 값을 설정해 화면이 변하지 않는 것처럼 활동할 수 있다.
예제:
const result = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
initialData: initialTodos,
})
쿼리와 달리 변화(mutation)는 CRUD에서 CUD를 담당하거나 서버 부작용을 보여준다.
예제:
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
변화는 다음과 같은 상태중 하나만 될 수 있다.
1. isIdle
또는 status === 'idle'
: 변화가 현재 휴식 되었거나 상태가 리셋되었거나 값을 새로 받았을 때
2. isPending
또는 status === 'pending'
: 변화가 진행중
3. isError
또는 status === 'error'
: 에러가 감지됨
4. isSuccess
또는 status === 'success'
: 변화가 성공적으로 진행됨 | 변화된 데이터 사용 가능
변화
는 비동기 함수이므로 React 16 버전 이하는 이를 사용할 수 없습니다.
이전 버전에서 사용하기 위해서는 onSubmit
안에 mutate
를 사용할 수 있습니다.
예제:
// This will not work in React 16 and earlier
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (event) => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// This will work
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (formData) => {
return fetch('/api', formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
변화 리셋하기:
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation({ mutationFn: createTodo })
const onCreateTodo = (e) => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Todo</button>
</form>
)
}
부작용 대비:
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
마찬가지로 재시도를 할 수 있고 이를 설정할 수 있습니다.
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})