Skip to content

react query

hwisaac edited this page Feb 21, 2023 · 1 revision

https://react-query-v3.tanstack.com/overview 리액트 쿼리 v3 공식문서

React Query

  • 리액트쿼리는 로직들을 축약해서 자동화 해준다.
  • 리액트쿼리는 받아온 데이터를 캐시에 저장해두기 때문에 데이터를 매번 fetch 하는데 로딩을 일으키지 않는다.

리액트쿼리 설치

npm install react query

리액트쿼리 사용

  1. queryClient 를 만든다.
  2. queryClientProvider 를 만든다. (provider는 그 자식요소들이 해당 provider로 접근하는 걸 의미한다.)
  3. 프로바이더로 컴포넌트들을 감싸준다.
  • QueryClientProviderclientprops 를 필요로 한다.
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <ThemeProvider theme={theme}>
    <App />
  </ThemeProvider>
</QueryClientProvider>;

useQuery 훅

useQuery 사용

  • const {isLoading, data} = useQuery("allMovies", fetchMovies);
  • useQuery("쿼리key", fetcher함수)fetcher를 가져오고 fetcher 가 로딩중에는 isLoadingtrue (data는 undefined), fetcher 가 로딩이 끝나면 false로 변경된다.
  • fetcher 가 끝나면 fetcher의 json리턴을 data 에 저장한다.
  • key 값은 유일해야 한다. key 값으로 캐시에 저장하고 불러와야 하기 때문
  • fetcher (쿼리함수)가 변수에 의존한다면 그 변수를 쿼리키에 넣어줘야 한다
 function Todos({ todoId }) {
   const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
 }

useQuery 훅이 리턴하는 값들 (몇 가지만 추렸음)

옵션명 타입 default 설명
status string "idle", "loading", "error", "success" 상태가 있다.
data TData undefined 쿼리를 성공한 resolved data
dataUpdatedAt number status==="success"로 가장최근 업데이트 된 쿼리의 타임스탬프
error null | TError null 쿼리에 대한 에러오브젝트
errorUpdatedAt number status==="error"로 가장최근 업데이트 된 쿼리의 타임스탬프
failureCount number 0 쿼리가 실패한 횟수
isError boolean status==="error" 의 값
isFetched boolean 쿼리가 패치된 이후인지 여부
isFetching boolean 쿼리가 패칭 도중인지 여부(백그라운드 패칭과 현재 패칭 둘다 포함)
isRefetching boolean isFetching && !isLoading 과 같은 의미
isIdle boolean status==="idle" 의 값
isLoading boolean status==="loading" 의 값
isLoadingError boolean true면 첫번째 패칭 도중 실패 했음을 의미
isPlaceholderData boolean 보여지는 data가 placeholder data인지 여부
isPreviousData boolean keepPreviousData가 켜졌을 때, 데이터가 이전 쿼리로 리턴된 데이터인지 여부
isRefetchError boolean 리패칭도중에 에러가 발생했을 경우
isStale boolean 캐시에 있는 데이터가 invalid거나 staleTime 으로 설정된 값보다 오래된 데이터인지 여부
isSuccess boolean status==="success" 의 값
refetch (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise 매뉴얼적으로 쿼리를 리패치 하는 함수
throwOnError: 쿼리 에러시 에러는 기록에만 남는다. 하지만 이 에러를 throw 하고 싶으면 켜라
cancelRefetch: 이게 true가 되면, 현재 요청된 request가 새로운 request가 만들어지기 전에 캔슬된다.
remove ()=>void 캐시에서 쿼리를 삭제해주는 함수

useQuery 훅의 두번째 인자 : fetcher 함수

  • 리액트쿼리를 쓸려면 우선 fetcher 함수를 만들어야 한다.
  • API 와 관련된 것들은 따로 모듈화 해서 컴포넌트들과 멀리 떨어져 있도록 하자 (컴포넌트가 fetch 하지 않도록) (권장)
  • 중요: fetcher 함수는 fetch promise (json date의 Promise)를 리턴해야 한다!
// ./src/api.ts
export function fetchCoins() {
  return fetch("https://api.coinpaprika.com/v1/coins").then((response) =>
    response.json()
  );
}

useQuery 훅의 세번째 인자 : option object

  • useQuery("쿼리key", fetcher, {...options} )
  • 세번째 Object 는 넣어도 되고 안넣어도 된다.
옵션명 타입 default 설명
refetchInterval number | false | ((data: TData | undefined, query: Query) => number | false) 숫자(ms)를 넣으면 주기마다 리패치,함수를 넣으면 주기를 계산해서 리턴
staleTime number | Infinity 0 시간(ms)이 지나면 데이터를 stale 상태로 바꾼다
cacheTime number | Infinity 5 * 60 * 1000 시간(ms)지나면 비활성화된 캐시 데이터가 메모리에서 사라짐
retryDelay number | (retryAttempt: number, error: TError) => number 숫자 및 리턴값은 재시도주기, retryAttempt 는 재시도 횟수(지수적으로 주기를 늘릴수 있음)
retryOnMount number | (retryAttempt: number, error: TError) => number true false면 에러시 마운트에 쿼리를 재시도 하지 않음
retry boolean | number | (failureCount: number, error: TError) => boolean false 실패시 재시도 할지 여부
onSuccess (data: TData) => void 새로운 데이터를 패치하거나 setQueryData로 캐시가 업데이트시 작동할 함수
onError (error: TError) => void 에러 발생시 작동할 함수
onSettled (data?: TData, error?: TError) => void 쿼리 결과에 상관없이 종료하면 작동할 함수
select (data: TData) => unknown 쿼리함수가 리턴한 데이터를 변경하거나 선택할때 사용할 함수
suspense boolean true면, useQuerystatus==='loading' 이면 연기되고, status==='error' 면 런타임 에러를 던짐
initialData TData | () => TData 초기 캐시 데이터 설정(stale 상태로 간주된다)
keepPreviousData boolean false 쿼리키 변경으로 패치가 되는 동안 이전 데이터가 유지된다.
refetchOnWindowFocus boolean | "always" | ((query: Query) => boolean | "always") true true면 데이터가 stale일때 윈도우 포커스만으로 refetch한다.
'always'면 stale 여부 따지지 않고 윈도우 포커스로 refetch한다.
refetchOnMount boolean | "always" | ((query: Query) => boolean | "always") true true면 데이터가 stale일때 마운트할때마다 refetch한다.
queryKeyHashFn (queryKey: QueryKey) => string 이 함수는 쿼리키를 문자열로 해싱한다
enable boolean true 자동으로 쿼리를 실행하는 기능의 on/off 여부

Devtools

설치: npm i @tanstack/react-query-devtools

  • 개발할 때 데이터를 확인할 수 있는 컴포넌트.
  • 랜더할수 있는 component
  • 리액트쿼리에 있는 devtoolsimport 해오면 캐시에 있는 query 를 볼 수 있다.

Devtools 사용

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

export default App;

mutation

  • query 와 달리 mutation 은 데이터를 create/update/delete 하거나 서버사이드에서 작용시킬때 사용되는게 일반적이다.

예제

 function App() {
   const mutation = useMutation(newTodo => {
     return axios.post('/todos', newTodo)
   })
 
   return (
     <div>
       {mutation.isLoading ? (
         '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>
   )
 }
  • 위의 예제에서 mutate 함수를 호출 할 때 single variable or object를 전달하는 것을 알수 있다.
  • 단순히 변수들과 함께 쓰는 것만으로는 mutation 이 특별한 것은 아니다. onSuccess 옵션으로, Query Client 의 invalidateQueries 메소드와 setQueryData 메소드를 함께 사용하면 mutations 는 훨씬 파워풀한 툴이 된다.

주의!
mutate 함수는 비동기함수이다. 따라서, 리액트 16이하 버전에서는 이벤트콜백으로 직접적으로 쓰지 않아야 한다.
onSubmit 이벤트에 접근하고 싶다면 mutate 함수를 다른 함수로 감싸야 한다. ( React event pooling 때문)

 // 리액트16 이하 버전에서는 작동 안함
 const CreateTodo = () => {
   const mutation = useMutation(event => {
     event.preventDefault()
     return fetch('/api', new FormData(event.target))
   })
 
   return <form onSubmit={mutation.mutate}>...</form>
 }
 
 // 이렇게 해야 작동함
 const CreateTodo = () => {
   const mutation = useMutation(formData => {
     return fetch('/api', formData)
   })
   const onSubmit = event => {
     event.preventDefault()
     mutation.mutate(new FormData(event.target))
   }
 
   return <form onSubmit={onSubmit}>...</form>
 }

Resetting Mutation state

  • 뮤테이션 요청의 dataerror 를 제거하고 싶을 때가 있는데, reset 함수를 사용하면 된다.
 const CreateTodo = () => {
   const [title, setTitle] = useState('')
   const mutation = useMutation(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>
   )
 }

Mutation Side Effects

  • useMutation 몇가지 헬퍼 옵션이 제공된다. 이를 통해서 mutation가 작동하는 각 스테이지마다 빠르고 쉽게 side-effects를 가능하게 한다.
 useMutation(addTodo, {
   onMutate: variables => {
     // mutation이 작동할 때 로직
 
     // 옵셔널하게 데이터를 포함하는 context를 리턴한다.
     return { id: 1 }
   },
   onError: (error, variables, context) => {
     // 에러 발생시
     console.log(`rolling back optimistic update with id ${context.id}`)
   },
   onSuccess: (data, variables, context) => {
     // 쿼리 성공시
   },
   onSettled: (data, error, variables, context) => {
     // 에러, 성공에 관계 없이 
   },
 })

연속적인 mutations

  • onSuccess, onError, onSettled 콜백들을 사용하는 것과 약간 차이가 있다.
  • mutate 함수에 전달되면, 이 mutations 들은 컴포넌트가 여전히 마운트 되어있는 동안에 한해 딱 한번만 작동한다.
  • 이것은 mutate함수가 호출될 때마다 mutation observer 가 제거되고 매번 다시 구독되기 때문이다.
  • 반면에 useMutation 핸들러들은 모든 mutate 콜을 실행시킨다.

주의
useMutation에 전달되는 mutationFn 는 아마 비동기 함수일건데, 이 경우 mutations 가 종료되는 순서는 호출 순서와 다를 수 있다.

 useMutation(addTodo, {
   onSuccess: (data, error, variables, context) => {
     // 3번 호출될 예정
   },
 })
 
 ['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
   mutate(todo, {
     onSuccess: (data, error, variables, context) => {
       // 어떤 mutation 이 제일 먼저 resolve 되는지와 관계 없이
       // 마지막 mutation인 Todo 3 에 대해 딱 한번만 실행된다.
     },
   })
 })

Promises

  • resolveerror 를 리턴할 Promise 를 갖고 싶다면 mutate 대신에 mutateAsync 를 사용해라.
const mutation = useMutation(addTodo)
 
 try {
   const todo = await mutation.mutateAsync(todo)
   console.log(todo)
 } catch (error) {
   console.error(error)
 } finally {
   console.log('done')
 }

Persist mutations (mutations 지속)

  • mutation 은 필요에 따라 스토리지에 저장될 수 있고 나중에 실행 될 수 있는데, 이는 hydration 함수를 사용해서 할 수있다.
 const queryClient = new QueryClient()
 
 // "addTodo" mutation 정의하기
 queryClient.setMutationDefaults('addTodo', {
   mutationFn: addTodo,
   onMutate: async (variables) => {
     // todos list 에 대한 현재 쿼리들을 취소한다.
     await queryClient.cancelQueries('todos')
 
     // optimistic todo 생성
     const optimisticTodo = { id: uuid(), title: variables.title }
 
     // todos list 에다 optimistic todo 를 추가 
     queryClient.setQueryData('todos', old => [...old, optimisticTodo])
 
     // optimistic todo 를 담은 context 를 리턴
     return { optimisticTodo }
   },
   onSuccess: (result, variables, context) => {
     // todos list 의 optimistic todo 를 result 로 교체한다.
     queryClient.setQueryData('todos', old => old.map(todo => todo.id === context.optimisticTodo.id ? result : todo))
   },
   onError: (error, variables, context) => {
     // todo list 에서 optimistic todo 를 삭제한다.
     queryClient.setQueryData('todos', old => old.filter(todo => todo.id !== context.optimisticTodo.id))
   },
   retry: 3,
 })
 
 // 컴포넌트에서 mutation 시작 
 const mutation = useMutation('addTodo')
 mutation.mutate({ title: 'title' })
 
 // 만약에 디바이스가 오프라인이 된다거나 해서 mutation 이 멈추게 된다면, 
 // 애플리케이션이 종료될 때 멈춘 mutation 이 dehydrated 될 것이다.
 const state = dehydrate(queryClient)
 
 // 다시 애플리케이션이 작동하면 이 mutation 은 다시 hydrated 된다.
 hydrate(queryClient, state)
 
 // 멈춘 mutation를 다시 시작
 queryClient.resumePausedMutations()

useMutation 훅

https://react-query-v3.tanstack.com/reference/useMutation

 const {
   data,
   error,
   isError,
   isIdle,
   isLoading,
   isPaused,
   isSuccess,
   mutate,
   mutateAsync,
   reset,
   status,
 } = useMutation(mutationFn, {
   mutationKey,
   onError,
   onMutate,
   onSettled,
   onSuccess,
   retry,
   retryDelay,
   useErrorBoundary,
   meta,
 })
 
 mutate(variables, {
   onError,
   onSettled,
   onSuccess,
 })

mutationFn : mutation 함수

  • mutationFn: (variables: TVariables) => Promise<TData>
  • 필수로 넣어야 한다.
  • 비동기적인 작업을 하고 promise를 리턴하는 함수 (일명 mutation 함수)
  • variablesmutationFn 에 넣어서 mutate 시킬 오브젝트이다.

useMutation 옵션

옵션명 타입 default 설명
mutationKey mutationKey: string mutationKey값은 queryClient.setMutationDefaults 또는 devtools 의 the mutation로 세팅 가능
onError (err: TError, variables: TVariables, context?: TContext) => Promise | void mutation이 error를 만났을때 작동하는 함수
promise가 리턴되면 await였다가 resolve한다
onMutate (variables: TVariables) => Promise<TContext | void> | TContext | void mutation함수가 작동하면 동일한 변수들을 인자로 실행될 함수
리턴되는 값은 onSettledonError에 전달된다.
업데이트 최적화에 유용할 수 있다.
onSettled (data: TData, error: TError, variables: TVariables, context?: TContext) => Promise | void mutation이 성공적으로 fetch하거나 error를 만났을때 실행될 함수
promise가 리턴되면, await 였다가 resolve한다
onSuccess (data: TData, variables: TVariables, context?: TContext) => Promise | void mutation이 fetch에 성공하면 그 결과를 인자로 받아서 작동한다.
promise 리턴시, await and resolved
retry boolean | number | (failureCount: number, error: TError) => boolean 0 false면 실패시 재시도 안함.
true면 실패마다 무한재시도
number면 숫자만큼 재시도
retryDelay number | (retryAttempt: number, error: TError) => number 함수면 retryAttemp는 재시도 횟수이고 리턴한 시간(ms)만큼 지연
예시 : attempt => attempt * 1000
useErrorBoundary undefined | boolean | (error: TError) => boolean undefined (global query config 에서 useErrorBoundary로 세팅) true면 랜더링도중에 mutation error 를 던지고 가장 가까운 error boundary로 전파한다
false면, 에러바운더리에 에러를 던지는 걸 disable한다.
함수에러를 받아서 error boundary로 그 에러를 나타낼지 여부를 리턴한다.
meta Record<string, unknown> 세팅하면, mutation cache 엔트리에 추가정보를 사용할 수 있다. mutation이 사용가능할때 접근할 수 있다.(예를들어 MutationCacheonError, onSuccess 함수들 )

useMutation 훅의 리턴

변수명 타입 default 설명
mutate (variables?: TVariables, { onSuccess, onSettled, onError }) => void mutation 함수를 호출하며 variables 를 넘겨준다.
여러 요청을 보낼경우 onSuccess 가 마지막 콜 이후에 작동하게 된다.
data undefined | unknown undefined 쿼리에 의해 마지막에 resolve된 데이터값
error null | TError 쿼리에 대한 에러 오브젝트
isError boolean status==="error"
isIdle boolean status==="idle"
isLoading boolean status==="loading"
isSuccess boolean status==="success"
mutate
mutateAsync (variables: TVariables, { onSuccess, onSettled, onError }) => Promise mutate랑 비슷한데, await할 수 있는 promise를 리턴해줌
reset () => void mutation internal state를 삭제함 (즉 초기 state로 mutation을 리셋)
status string "idle" :mutation함수 작동 전 초기상태
"loading":mutation함수 실행중
"error":마지막 mutation이 에러났는지
"success": 마지막 mutation이 성공했는지

useInfiniteQuery

  • 이미 가지고 있는 데이터 세트에다가 추가적으로 데이터를 로드 해야될 경우 사용한다. (ex: 무한스크롤 or 페이지네이션 )
  • useInfiniteQuery 를 사용할 때 알아야 하는 사실들
    1. data 는 현재 infinite query data를 포함하는 객체이다.
    2. data.pages 는 패치받아온 페이지들로 이뤄진 배열이다.
    3. data.pageParams 는 페이지들을 패치할때 사용한 page params 로 이뤄진 배열이다.
    4. fetchNextPagefetchPreviousPage 눈 둘다 'now available'인 함수들
    5. getNextPageParamgetPreviousParam 은 만약 추가로 더 가져올 데이터가 있는지 판단하는 옵션들이다. 이 정보는 쿼리 함수에 추가 파라미터로서 제공된다. (쿼리함수는 fetchNextPage와 fetchPreviousPage 호출로 덮어쓸 수도 있다.)
    6. hasNextPage/hasPreviousPage 는 'now available'인지 나타내는 boolean 이다.(getNextPageParam/getPreviousPageParamundefined를 리턴하지 않을 경우 true가 된다는 말)
    7. isFetchingNextPageisFetchingPreviousPage 는 백그라운드의 refresh state 인지 state를 추가로 load할 수 있는지를 나타내는 boolean이다

예시

// cursor 인덱스로 한번에 페이지를 세개씩 가져온다고 할 때

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: [...] }
  • 이러한 정보로 Load More 버튼 같은 걸 만들 수 있다.

주의!
getNextPageParam에서 리턴한 pageParam 데이터를 덮어쓰기를 원하는게 아니라면, fetchNextPage를 콜백 인자로 넣어서 호출하지 말자!
예를들어, <button onClick={fetchNextPage} /> 이런식으로 쓰지 말자.

import { useInfiniteQuery } from 'react-query'
 
 function Projects() {
   const fetchProjects = ({ pageParam = 0 }) =>
     fetch('/api/projects?cursor=' + pageParam)
 
   const {
     data,
     error,
     fetchNextPage,
     hasNextPage,
     isFetching,
     isFetchingNextPage,
     status,
   } = useInfiniteQuery('projects', fetchProjects, {
     getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
   })
 
   return status === 'loading' ? (
     <p>Loading...</p>
   ) : status === 'error' ? (
     <p>Error: {error.message}</p>
   ) : (
     <>
       {data.pages.map((group, i) => (
         <React.Fragment key={i}>
           {group.projects.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>
     </>
   )
 }

사용예시2

import {  useSearchParams } from "react-router-dom";
import { useInfiniteQuery } from "react-query";

// fetcher 함수
function searchData(
  category: string,
  keyword: string,
  page: number
) {
  const json = await fetch(
    `${BASE_PATH}/search/${category}?api_key=${API_KEY}&query=${keyword}&page=${page}`
  ).then((response) => response.json());
  
  return json;
}


export default function Search() {
// 검색어를 제출하면 Search 가 랜더링 되는데, 검색어 키워드를 가져온다.
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("keyword");

// 랜더링 될 영화들이 담길 state
const [movies, setMovies] = useState();

// useInfiniteQuery 적용
const {
    data: movieInfiniteData,
    fetchNextPage: movieFetchNextPage,
    hasNextPage: movieHasNextPage,
    isFetchingNextPage: movieIsFetchingNextPage,
  } = useInfiniteQuery(
    ["searchMovie", keyword],
    ({ pageParam = 1 }) => searchData("movie", keyword, pageParam),
    {
      getNextPageParam: (lastPage, pages) => {
        const { page, total_pages } = lastPage;
        return page < total_pages ? page + 1 : undefined;
      },

      onSuccess: (movieInfiniteData) => {
        let result = [];
        for (let page of movieInfiniteData.pages) {
          result = [...result, ...page.results];
        }
        setMovies(result);
      },
    }
  );

return (
<Wrapper>
  <Title>Movie SEARCH: {keyword}</Title>

  {movies && <SearchedResults results={movies} />}

  <MoreButton
    onClick={() => movieFetchNextPage()}
    disable={!movieHasNextPage || movieIsFetchingNextPage}>
    {movieIsFetchingNextPage
      ? "추가 패칭중"
      : movieHasNextPage
      ? "영화 더보기"
      : "남은 영화가 없습니다."}
  </MoreButton>
</Wrapper>
);
}

useInfiniteQuery Hook

const {
   fetchNextPage,
   fetchPreviousPage,
   hasNextPage,
   hasPreviousPage,
   isFetchingNextPage,
   isFetchingPreviousPage,
   ...result
 } = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
   ...options,
   getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
   getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
 })

옵션

  • useInfiniteQuery 의 옵션들은 useQuery 훅이랑 같은데, 아래 내용이 추가된다.
옵션명 타입 default 설명
queryFn (context: QueryFunctionContext) => Promise defaultQueryFn - defaultQueryFn이 없는 경우에 필수
- 데이터 요청할때 사용하는 쿼리 함수
QueryFunctionContextqueryKeypageParam:unknown|undefined변수를 갖는 객체이다
- datapageParam(getNextPageParam을 사용시)을 리턴해야 한다.
getNextPageParam (lastPage, allPages) => unknown | undefined - 쿼리의 신규데이터를 받으면 getNextPageParam 함수에 infinite data list의 마지막 페이지와 페이지들의 full array가 인자로 전달된다
- single variable을 리턴해야 한다. 이 단일 변수는 쿼리함수의 마지막 옵셔널 파라미터에 전달된다.
- undefined를 리턴시키면 다음페이지가 없음을 가리킨다.
getPreviousPageParam (firstPage, allPages) => unknown | undefined - 쿼리의 신규데이터를 받으면 getPreviousParam 함수에 infinite data list의 첫 페이지와 모든 페이지가 인자로 전달된다.
- single variable 을 리턴해야 되는데, 이 단일 변수는 쿼리의 마지막 옵셔널 파라미터에 전달된다.
-undefined를 리턴면 이전 페이지가 없다는 걸 가리킨다.

리턴값

  • useInfiniteQuery 의 리턴은 useQuery 훅이랑 동일한데, 아래 내용이 추가된다.
옵션명 타입 default 설명
data.pages TData[] 모든 페이지를 담은 배열
data.pageParams unknown[] 모든 page params를 담은 배열
isFetchingNextPage boolean fetchNextPage로 다음 페이지가 패칭되는 동안 true
isFetchingPreviousPage boolean fetchPreviousPage로 다음 페이지가 패칭되는 동안 true
fetchNextPage (options?: FetchNextPageOptions) => Promise true 이 함수는 results의 다음 page를 패치하게 해준다.
options.pageParam: unknown는 특정한 page param을 사용해서 패칭하게 해준다.
options.cancelRefetch: booleantruefetchNextPage를 반복 호출하면 fetchPage도 매번 호출되고 또한 이전의 호출은 무시된다. falsefetchNextPage를 반복호출해도 첫번째 호출이 resolved가 되기전까지 기다린다.
fetchPreviousPage (options?: FetchPreviousPageOptions) => Promise 위와 비슷
hasNextPage boolean next page를 fetch할게 있으면 가 된다.(getNextPageParam옵션을 통해 정해짐)
hasPreviousPage boolean 위와 비슷

useQueries 훅

  • 쿼리를 여러개 할 때 사용한다.
  • 옵션과, 리턴값은 useQuery 의 옵션과 리턴 객체를 배열로 반환한다.
const results = useQueries([
  { queryKey: ["post", 1], queryFn: fetchPost },
  { queryKey: ["post", 2], queryFn: fetchPost },
]);

useIsFetching

  • 너의 앱에서 백그라운드상에서 loading 또는 fetching 중인 쿼리들의 갯수를 리턴해준다.
import { useIsFetching } from "react-query";
// 얼마나 많은 패칭중인 쿼리가 있는가?
const isFetching = useIsFetching();
// 'posts' 접두사를 가진 쿼리중에서 얼마나 많은 패칭 쿼리가 있는지 리턴
const isFetchingPosts = useIsFetching(["posts"]);

옵션

  • queryKey?: QueryKey
  • filters?: QueryFilter

리턴

  • isFetching: number

useIsMutating

가이드: Paginated/Lagged Querries

  • 페이지로 나타낸 데이터는 자주 쓰이는 UI 패턴이다. 리액트 쿼리에서는 쿼리키에 page 를 포함시킴으로써 기능을 구현 시킬수 있다.
const result = useQuery(["project", page], fetchProjects);

만약 비슷한 예제를 실행해본 적 있다면 다음과 같은 점을 깨달을 수 있을 것이다

각 새 페이지는 새 쿼리처럼 처리되기 때문에 UI는 success 및 loading 상태를 반복한다.

  • 이러한 사용자 경험은 좋은 것이 아니며, 안타깝게도 오늘날 많은 툴들이 이런 형태의 작동방식을 고집하고 있다.
  • 그러나 리액트 쿼리는 keepPreviousData 라는 기능을 통해 놀라운 기능을 제공한다.

keepPreviousData 를 이용한 쿼리 페이지네이션

  • 간단한 예제를 보자. 우리는 pageIndex( 또는 cursor )를 쿼리에 대해 증가시키면서 페이지네이션을 구현한다.
  • useQuery 를 사용한다면 이 또한 기능적으로는 잘 작동할 것이다. 하지만, 니가 만든 UI 는 각 페이지(또는 cursor) 마다 successloading 상태를 반복하며 서로 다른 쿼리들이 생성됐다가 소멸한다.
  • keepPreviousDatatrue 로 세팅해보자!

keepPreviousData : true

  1. 마지막으로 패치해온 데이터는 새로운 데이터가 요청되는 동안에도(쿼리키가 변경되어도) 살아있게 된다.
  2. 새로운 데이터가 도착하면, 이전의 데이터는 빈틈 없이 새로운 데이터를 보여주기 위해 교체된다.
  3. isPreviousData 는 쿼리가 현재 제공하는 데이터가 어떤 건지 너에게 알려주기 위해 생성된다.
function Todos() {
  const [page, setPage] = React.useState(0);

  const fetchProjects = (page = 0) =>
    fetch("/api/projects?page=" + page).then((res) => res.json());

  const { isLoading, isError, error, data, isFetching, isPreviousData } =
    useQuery(["projects", page], () => fetchProjects(page), {
      keepPreviousData: true,
    });

  return (
    <div>
      {isLoading ? (
        <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 (!isPreviousData && data.hasMore) {
            setPage((old) => old + 1);
          }
        }}
        // 다음 페이지가 available 일때 까지 Next 버튼을 비활성화
        disabled={isPreviousData || !data?.hasMore}>
        Next Page
      </button>
      {isFetching ? <span> Loading...</span> : null}{" "}
    </div>
  );
}

페이지네이션 예제 (v4 버전)

import React from "react";
import axios from "axios";
import {
  useQuery,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}

async function fetchProjects(page = 0) {
  const { data } = await axios.get("/api/projects?page=" + page);
  return data;
}

function Example() {
  const queryClient = useQueryClient();
  const [page, setPage] = React.useState(0);

  const { status, data, error, isFetching, isPreviousData } = useQuery({
    queryKey: ["projects", page],
    queryFn: () => fetchProjects(page),
    keepPreviousData: true,
    staleTime: 5000,
  });

  // Prefetch the next page!
  React.useEffect(() => {
    if (!isPreviousData && data?.hasMore) {
      queryClient.prefetchQuery({
        queryKey: ["projects", page + 1],
        queryFn: () => fetchProjects(page + 1),
      });
    }
  }, [data, isPreviousData, page, queryClient]);

  return (
    <div>
      <p>
        In this example, each page of data remains visible as the next page is
        fetched. The buttons and capability to proceed to the next page are also
        supressed until the next page cursor is known. Each page is cached as a
        normal query too, so when going to previous pages, you'll see them
        instantaneously while they are also refetched invisibly in the
        background.
      </p>
      {status === "loading" ? (
        <div>Loading...</div>
      ) : status === "error" ? (
        <div>Error: {error.message}</div>
      ) : (
        // `data` will either resolve to the latest page's data
        // or if fetching a new page, the last successful page's data
        <div>
          {data.projects.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </div>
      )}
      <div>Current Page: {page + 1}</div>
      <button
        onClick={() => setPage((old) => Math.max(old - 1, 0))}
        disabled={page === 0}>
        Previous Page
      </button> <button
        onClick={() => {
          setPage((old) => (data?.hasMore ? old + 1 : old));
        }}
        disabled={isPreviousData || !data?.hasMore}>
        Next Page
      </button>
      {
        // Since the last page's data potentially sticks around between page requests,
        // we can use `isFetching` to show a background loading
        // indicator since our `status === 'loading'` state won't be triggered
        isFetching ? <span> Loading...</span> : null
      }{" "}
      <ReactQueryDevtools initialIsOpen />
    </div>
  );
}

lagging infinite query results with keepPreviousData

  • keepPreviousData 옵션은 useInfiniteQuery 훅이랑 같이 써도 잘 어울린다.
  • 그래서 계속 infinite query keys가 바뀌는 동안에도 너는 사용자들에게 캐시된 데이터를 빈틈 없이 연속으로 보여줄 수 있다.

Default Query Function

  • default query function 을 아용하면 쿼리함수를 앱 전체에서 사용할수 있고, 쿼리키를 사용해서 가져올 항목을 식별한다.
// 쿼리 키를 받게 될 default쿼리함수를 정의
// 쿼리키는 배열로 만드는 걸로 보장
const defaultQueryFn = async ({ queryKey }) => {
  const { data } = await axios.get(
    `https://jsonplaceholder.typicode.com${queryKey[0]}`
  );
  return data;
};

// defaultOptions 를 이용해서 default 쿼리함수를 앱에 제공
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: defaultQueryFn,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

// 이제 니가 해야 할 것은 key 를 전달해주는 것 뿐임
function Posts() {
  const { status, data, error, isFetching } = useQuery("/posts");

  // ...
}

// 쿼리함수를 생략하고 옵션으로 바로 이동할 수도 있다
function Post({ postId }) {
  const { status, data, error, isFetching } = useQuery(`/posts/${postId}`, {
    enabled: !!postId,
  });

  // ...
}

QueryClient 클래스 [문서]

  • QueryClient 는 캐시와 상호작용 할 때 사용한다
import { QueryClient } from "react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
    },
  },
});

await queryClient.prefetchQuery("posts", fetchPosts);

// await queryClient.prefetchQuery({queryKey: ["posts"], queryFn: fetchPosts});  이건 v4버전

options

  • queryCache?: QueryCache
    • Optional
    • The query cache this client is connected to.
  • mutationCache?: MutationCache
    • Optional
    • The mutation cache this client is connected to.
  • defaultOptions?: DefaultOptions
    • Optional
    • Define defaults for all queries and mutations using this queryClient.

methods

  • queryClient.fetchQuery
  • queryClient.fetchInfiniteQuery
  • queryClient.prefetchQuery
  • queryClient.prefetchInfiniteQuery
  • queryClient.getQueryData
  • queryClient.getQueriesData
  • queryClient.setQueryData
  • queryClient.getQueryState
  • queryClient.setQueriesData
  • queryClient.invalidateQueries
  • queryClient.refetchQueries
  • queryClient.cancelQueries
  • queryClient.removeQueries
  • queryClient.resetQueries
  • queryClient.isFetching
  • queryClient.isMutating
  • queryClient.getDefaultOptions
  • queryClient.setDefaultOptions
  • queryClient.getQueryDefaults
  • queryClient.setQueryDefaults
  • queryClient.getMutationDefaults
  • queryClient.setMutationDefaults
  • queryClient.getQueryCache
  • queryClient.getMutationCache
  • queryClient.clear

메소드 설명

  1. queryClient.fetchQuery
  • 데이터를 리졸브 하거나 에러를 던지는 비동기 메소드이다. (패치하거나 쿼리를 캐시할 때 사용)
    • 만약 result 가 필요하지 않은 채 쿼리를 패치 해야 되는 거면 prefetchQuery 를 대신 사용해라
  • 쿼리가 존재하고, 데이터가 invalidated 거나 오래돼서 stale 이면 캐시에 있던 데이터를 리턴한다. 반면 최신 데이터를 패치하려고 시도한다.

fetchQuerysetQueryData 의 차이
fetchQuery는 비동기적이며 데이터를 가져오는 동안 동일한 쿼리에 대한 useQuery 인스턴스를 사용하여 이 쿼리에 대한 중복 요청이 생성되지 않도록 한다

try {
  const data = await queryClient.fetchQuery(queryKey, queryFn);
} catch (error) {
  console.log(error);
}
  • fetchQueryoptions 는 useQuery 랑 동일한데
  • enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, notifyOnChangePropsExclusions, onSuccess, onError, onSettled, useErrorBoundary, select, suspense, keepPreviousData, placeholderData 이 옵션들은 useQueryuseInfiniteQuery 에만 있다.
  1. queryClient.fetchInfiniteQuery
  • fetchQuery 랑 비슷한데 fetch 하고 infinite query 를 캐시할 때 사용한다.
try {
  const data = await queryClient.fetchInfiniteQuery(queryKey, queryFn);
  console.log(data.pages);
} catch (error) {
  console.log(error);
}
  1. queryClient.prefetchQuery
  • 이 메소드는 비동기 메소드다. useQuery 와 그 친구들을 랜더링 하거나 필요로 하기 전에 미리 패칭할 때 사용한다.
  • 이 메소드의 옵션은 fetchQuery 랑 비슷하다. 다른 점은 얘는 아무것도 리턴하지 않는다.
await queryClient.prefetchQuery(queryKey, queryFn);
await queryClient.prefetchQuery(queryKey); // default queryFn 을 설정했을 때
  1. queryClient.prefetchInfiniteQuery
  • prefetchQuery 랑 비슷한데, prefetch 하고 infinite query 에 캐시한다
  • 옵션은 fetchQuery 랑 동일
await queryClient.prefetchInfiniteQuery(queryKey, queryFn);
  1. queryClient.getQueryData
  • 이 메소드는 동기적인 함수다. 이미 캐시에 존재하는 데이터를 사용할때 쓴다.
  • 쿼리키나 쿼리필터를 넘겨서 이에 해당하는 쿼리들을 리턴해준다. (매칭되는게 없으면 빈배열 리턴)
const data = queryClient.getQueriesData(queryKey | filters);
  1. queryClient.setQueryData
  • 동기함수이다. 쿼리의 캐시데이터를 즉시 업데이트 할때 사용한다. 해당 쿼리가 없다면 생성해준다.
  • 업데이트 된 쿼리가 쿼리 hook에 의해 사용되지 않을 경아 캐시타임이 지나가면 쿼리는 가비지 콜렉트(캐시에서 제거)됩니다.
queryClient.setQueryData(queryKey, updater);
  • queryKey: QueryKey: Query Keys
  • updater: TData | (oldData: TData | undefined) => TData
    • updater 에 함수가 아닌 걸 전달하면, 해당 값으로 데이터가 업데이트 됨
    • updater 에 함수가 전달되면 이전 old data를 매개변수로 받고 업데이트 할 새로운 값을 리턴한다.
  1. queryClient.getQueryState
  • 동기 함수로 존재하는 쿼리 스테이트를 가져올때 사용한다. (없으면 undefined를 리턴한다.)
const state = queryClient.getQueryState(queryKey);
console.log(state.dataUpdatedAt);
  1. queryClient.setQueriesData
  • 동기 함수로, 필터함수를 이용해 캐시에서 매칭되는 여러 쿼리들을 업데이트 한다.
  1. queryClient.invalidateQueries
  • 캐시에 있는 쿼리(들)을 무효화 하거나 refetch 할때 사용한다.
await queryClient.invalidateQueries(
  "posts",
  {
    exact,
    refetchActive: true,
    refetchInactive: false,
  },
  { throwOnError, cancelRefetch }
);
  1. queryClient.refetchQueries
  • 특정 조건에서 쿼리들을 리패치 할 때 사용한다.
// refetch all queries:
await queryClient.refetchQueries();

// refetch all stale queries:
await queryClient.refetchQueries({ stale: true });

// refetch all active queries partially matching a query key:
await queryClient.refetchQueries(["posts"], { active: true });

// refetch all active queries exactly matching a query key:
await queryClient.refetchQueries(["posts", 1], { active: true, exact: true });
  1. queryClient.cancelQueries
  • 캐시에서 outgoing 하는 쿼리들을 캔슬할 때 사용한다.
  • 최적의 update 를 수행시 사용하면 유용하다.
await queryClient.cancelQueries("posts", { exact: true });
  1. queryClient.removeQueries
  • 캐시된 쿼리들을 제거할 때 사용한다.
  1. queryClient.resetQueries
  • 초기 스테이트로 캐시에 있는 쿼리들을 초기화 할때 사용한다.
  • clear는 모든 구독자를 모두 제거해버리지만, resetQueries는 구독자에게 리셋을 알리고 사전 로드된 state로 리셋시킵니다.
  • 쿼리가 initialData 가 있으면, 이걸로 리셋시킨다.
  • 쿼리가 active 면, 리패치된다.
queryClient.resetQueries(queryKey, { exact: true });
  1. queryClient.isFetching
  2. queryClient.isLoading
  3. queryClient.isMutating
  4. queryClient.getDefaultOptions
  • 클라이언트를 생성할때 세팅된 default options (or setDefaultOptions) 를 리턴한다.
const defaultOptions = queryClient.getDefaultOptions();
  1. queryClient.setDefaultOptions
  • 이 쿼리 클라이언트에 동적으로 default options 를 설정할 때 사용한다. 이전 값은 덮어쓰기 됨
queryClient.setDefaultOptions({
  queries: {
    staleTime: Infinity,
  },
});
  1. queryClient.getQueryDefaults
  • 특정 쿼리에 대한 default options 를 리턴한다.
const defaultOptions = queryClient.getQueryDefaults("posts");
  1. queryClient.setQueryDefaults
  • 특정 쿼리에 대한 default options 를 설정할 때 사용한다
queryClient.setQueryDefaults("posts", { queryFn: fetchPosts });

function Component() {
  const { data } = useQuery("posts");
}
  1. queryClient.clear
  • 연결된 모든 캐시를 제거한다
queryClient.clear();