보통 서버 state(ajax를 통해 서버에서 가져오는 데이터)가 있고 클라이언트 state가 있는데 데이터 fetching 해서 불러오는 데이터는 서버 state로 관리하는 게 좋다.
React Query의 존재를 알기 전엔 서버에서 가져온 데이터를 internal state 관리를 위한 useState, context api 를 사용하거나, redux, recoil 등의 상태관리 툴을 사용하여 저장하고 가공하여 react 내부에서 사용해주었었다.
하지만, 서버 state는 주로 비동기적으로 다루게 되고, 클라이언트 state는 주로 동기적으로 다루게 되므로 따로 관리해주는 것이 좋다.
React Query는 이를 쉽게 할 수 있게 도와준다.
stale queries가 자동으로 refetch 되는 조건은 다음과 같다.
- New instances of the query mount - 새 query가 mount 될 때
- The window is refocused - 윈도우 화면을 focus 하였을 때(refetchOnWindowFocus와 같은 옵션을 줘서 refetch되지 않도록 해줄 수 있다. 공식문서)
- The network is reconnected. - 네트워크가 재연결 되었을 때
- The query is optionally configured with a refetch interval. - cacheTime이 지나 refetch가 될 때
staleTime은 data 가 fresh -> stale 로 가는 데 걸리는 시간을 말하는 것이다. Data가 fresh 에 있는 동안은 데이터 fetch가 되지 않는다.(query가 unmount 되었다가 mount되어도.)
fresh 이면, 캐시에 저장된 data 를 그대로 보여준다.
cacheTime은 data 가 inactive 상태가 시작된 서부터 cache 유지 시간을 말하는 것이고, cacheTime 이 지나기 이전에 fetch 가 되면 데이터 fetch가 이루어진다. 데이터 fetch가 이루어지는 동안 캐싱된 previousData를 띄워줄 수가 있다.
staleTime 과는 관계없이 inactive 상태가 시작된 기준이다.
쿼리 결과는 기본적으로 구조적으로 공유되어 데이터가 실제로 변경되었는지 여부를 감지하고, 그렇지 않은 경우 데이터 참조는 변경되지 않은 상태로 유지되어 useMemo 및 useCallback과 관련하여 값 안정화에 더 도움이 된다.
캐싱된 Data가 없을 때 첫 fetching 되는 로딩 상태
캐싱된 Data가 있는 경우 reFetching 될 때의 로딩 상태
const result = useQuery(['todos'], fetchTodoList)
위의 result object는 크게 세 가지 status 를 가진다.
isLoading or status === 'loading' - The query has no data yet
isError or status === 'error' - The query encountered an error
isSuccess or status === 'success' - The query was successful and data is available
위의 result object에는 세 가지의 fetchStatus 를 가진다.
fetchStatus === 'fetching' - The query is currently fetching.
fetchStatus === 'paused' - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide.
fetchStatus === 'idle' - The query is not doing anything at the moment.
status 와 fetchStatus 의 차이점은 무엇일까? 왜 두 가지 state가 있는 것일까?
상태는 success status
임과 동시에 idle fetchStatus
나 fetching fetchStatus
가 될 수 있다. Fetching fetchStatus
인 경우엔 백그라운드에서 refetch가 일어나고 있을 경우를 얘기한다.
즉, success status
는 이미 data fetch가 성공적으로 이루어진 경우에 query가 data를 가지고 있을 때를 얘기하므로, data 를 가지고 있는 상태에서 뒤에서는 새로운 refetching이 이루어지고 있는 것 일 수 있는 것이다.
Query가 mount 되었지만 data 가 없는 경우는 loading status
거나 fetching fetchStatus
가 대부분이다. 하지만 네트워크 연결 오류로 인한paused fetchStatus
일 수도 있다.
즉, status
는 data 정보를 가지고 있는 지 여부에 따른 상태고, fetchStatus
는 queryFunction 이 실행되고 있는 지에 따른 상태를 말한다고 할 수 있다.
즉, 위에서 설명한 isFetching 의 경우도 status 는 loading일지라도 background에서 fetching 중일 경우 띄워줄 수 있는 것이다.
설정하는 쿼리 키에 따라 React Query는 query 캐싱을 다루게 된다.
useQuery(['todos'], ...)
쿼리 키는 위와 같이 배열로 나타내야 한다.
useQuery(['todos', { status, page }], ...)
위와 같이 배열이 아닌 객체의 형태로 key를 주게 되면 queries 는 일치하는 것으로 여겨지게 된다.
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
}
query 함수가 변수에 의존하고 있으면, query key에 포함 시켜라
쿼리가 자동으로 불러와지는 것을 disable 시키기 위해서는 enable 을 false 로 설정하면 된다.
enable: false
일 경우,
- 캐싱된 데이터가 있는 경우 =>
status === 'success'
나isSuccess
상태에서 query 시작- 캐싱된 데이터가 없는 경우 =>
status === 'loading'
그리고fetching === idle
상태에서 query 시작- 쿼리가 자동으로 fetch, refetch 되지 않음
- refetch를 하게 하는 invalidateQueries 와 refetchQueries 호출을 무시
쿼리가 생성될 때 함께 호출이 되는 것을 막기 위해서 lazy queies 를 사용할 수 있다. 원하는 때에 호출이 되게끔 하게하기 위해서 다음과 같이 사용해줄 수 있다.
const { data } = useQuery(
['todos', filter],
() => fetchTodos(filter),
{
// ⬇️ disabled as long as the filter is empty
enabled: !!filter
}
)
filter가 true일 때 쿼리가 fetch된다.
위와 같이 status === 'loading'
이지만 fetching === 'idle'
인 상황에서는 isLoading flag를 사용해줄 수 없다. 실제로 fetching이 되고 있는 상태가 아니기 때문이다. 이럴 때, isInitialLoading
를 사용하여 로딩을 보여줄 수가 있다.
keepPreviousData: true
와 같이 하면 refetch 되는 동안 전에 캐싱된 데이터를 보여준다.
React Query는 페이지네이션과 무한 스크롤 기능을 유용하게 해준다.
paginated queries 공식문서
infinite queries 공식문서
initialData
와 유사하지만 캐싱이 되지 않는다는 점에서 차이점이 있다. 실제 데이터가 fetch 되기 전에 보여줄 데이터를 설정하는 데 쓰이며, 다음과 같이 다른 query 로 부터 이미 캐싱된 데이터를 불러와 그 데이터를 placeholder query data 로 써줄 수 있다.
function Todo({ blogPostId }) {
const result = useQuery(['blogPost', blogPostId], () => fetch(`/blogPosts/${blogPostId}`), {
placeholderData: () => {
// Use the smaller/preview version of the blogPost from the 'blogPosts' query as the placeholder data for this blogPost query
return queryClient
.getQueryData(['blogPosts'])
?.find(d => d.id === blogPostId)
},
})
}