react-query guide - Queries

React query - Queries

React-query Essentials를 듣고 작성한 문서입니다.

대부분의 코드는 react-query 인강에 나오는 코드를 사용하였고, 그 이외의 이해를 돕기위한 코드는 react-query 공식 문서에 나오는 코드를 사용하였습니다.

인강에 나오는 코드 레파지토리

react-query 공식문서



1. Basic query

  • useQuery('uniqueKey', api function )

  • useQuery에 uniqueKey가 있어야 하고 중복되면 안된다

    const queryInfo = useQuery('pokemon', () =>
        axios
          .get('https://pokeapi.co/api/v2/pokemon')
          .then(res => res.data.results)
      )

2. Loading, Error state

  • 로딩, 에러 상태

  • 리액트 쿼리는 기본적으로 useQueryf 를 썼을 때 isLoading과 isError 값을 가지고 있다

    const { data, isLoading, isError } = useQuery('uniqueKey', getSomething)
    const queryInfo = useQuery('pokemon', async () => {
        await new Promise(resolve => setTimeout(resolve, 1000))
        // if (true) {
        //   throw new Error('Test error!')
        // }
        return axios
          .get('https://pokeapi.co/api/v2/pokemon')
          .then(res => res.data.results)
      })
    
      return queryInfo.isLoading ? ( //로딩 처리
        'Loading...'
      ) : queryInfo.isError ? ( //에러 처리
        queryInfo.error.message
      ) : (
        <div>
          {queryInfo.data.map(result => {
            return <div key={result.name}>{result.name}</div>
          })}
        </div>
      )
    

3. refetching queries on window focus

  • 다른 탭이나 다른 창에서 무언가를 하다가 다시 돌아왔을 때 윈도우가 포커스 되면 리액트 쿼리는 그 값들을 다시 조회한다

  • 기본값은 true 이고 false로 바꾸고 싶다면 설정 값을 추가하면 된다,

      const queryInfo = useQuery(
        'pokemon',
        async () => {
          await new Promise(resolve => setTimeout(resolve, 1000))
          return axios
            .get('https://pokeapi.co/api/v2/pokemon')
            .then(res => res.data.results)
        },
        {
          refetchOnWindowFocus: false,
        }
      )

3. isFetching

  • 위에서 윈도우에 포커스가 됐을 때 사용자는 화면 뒤에서 무슨 일이 벌어지고 있는지 모르기 때문에 isFetching 이라는 값을 이용해 데이터가 업데이트 중임을 표시할 수 있다

    const queryInfo = useQuery('pokemon', async () => {
        await new Promise(resolve => setTimeout(resolve, 1000))
        return axios
          .get('https://pokeapi.co/api/v2/pokemon')
          .then(res => res.data.results)
      })
    
      return (
        <div>
          {queryInfo.isFetching ? 'Updating...' : null}
        </div>
      )
    }
    

4. Stale time

Stale time : fresh -> stale 한 상태로 변경되는데 걸리는 시간

  • useQuery나 useInfiniteQuery는 기본적으로 cashe 된 데이터를 stale이라고 여긴다, stale한 query는 아래의 조건이 되면 자동적으로 refetch한다

    • automatically Refetch 조건
      • New instances of the query mount
      • The window is refocused
      • The network is reconnected.
      • The query is optionally configured with a refetch interval.
  • staleTime을 설정하면 그 시간동안 query의 상태가 fresh 상태로 되기 때문에 재조회를 하지 않는다

  • default : 0 , global로 설정 가능하다

    staleTime의 type

    staleTime: number | Infinity
    • 사용법
    const queryInfo = useQuery(
        'pokemon',
        async () => {
          await new Promise(resolve => setTimeout(resolve, 1000))
          return axios
            .get('https://pokeapi.co/api/v2/pokemon')
            .then(res => res.data.results)
        },
        {
          staleTime: Infinity, //staleTime: 5000
        }
      )
    

5. Cache Time

Cache Time : inactive (미사용, 비활성) 상태일 때 cache 된 상태로 메모리에 남아있는 시간

  • inactive 상태일 때 cache time 이후 garbage 로 들어간다

  • cache time 일 때 쿼리를 재조회 하면 재조회 하는 동안 이전에 캐시된 데이터를 보여준다 (isFetching 활성화)

  • cache time 이 끝나고 재조회 하면 처음부터 쿼리를 다시 조회한다 (isLoading 활성화)

  • cache time : infinity 는 cache time을 비활성화 한다는 뜻이다

    cacheTime type

    cacheTime: number | Infinity

    사용법

    const queryInfo = useQuery(
        'pokemon',
        async () => {
          await new Promise(resolve => setTimeout(resolve, 1000))
          return axios
            .get('https://pokeapi.co/api/v2/pokemon')
            .then(res => res.data.results)
        },
        {
          cacheTime: Infinity,
        }
      )

6. Query key 와 caching

  • 같은 내용이라도 서로 다른 query Key 로 했을 경우
    • request 두 번 요청
    • 서로 다른 값으로 캐싱
  • 서로 같은 query Key 로 설정했을 경우
    • 2개의 instance
    • request 한 번
    • 이 쿼리는 한 값으로 캐싱된다
    export default function App() {
      return (
        <div>
          <Pokemon queryKey="pokemon1" />
          <!-- <Pokemon queryKey="pokemon1" /> -->
          <Pokemon queryKey="pokemon2" />
          <ReactQueryDevtools />
        </div>
      )
    }
    
    function Pokemon({ queryKey }) {
      const queryInfo = useQuery(queryKey, async () => {
        await new Promise(resolve => setTimeout(resolve, 1000))
        return axios
          .get('https://pokeapi.co/api/v2/pokemon')
          .then(res => res.data.results)
      })
    
      return queryInfo.isLoading ? (
        'Loading...'
      ) : queryInfo.isError ? (
        queryInfo.error.message
      ) : (
        <div>
          {queryInfo.data.map(result => {
            return <div key={result.name}>{result.name}</div>
          })}
          <br />
          {queryInfo.isFetching ? 'Updating...' : null}
        </div>
      )
    }

7. Custom hook으로 쿼리 재사용 하기

  • 앱이 커지다 보면 동일한 데이터 조회를 여러 컴포넌트에서 사용해야 할 때가 있다.

  • custom hook을 만들어 여러 컴포넌트에서 동일한 키를 가진 리액트 쿼리를 사용하는 방법이다. (동일한 key 로 여러 요청을 만들어도 된다. 하나의 요청으로 인식하니까 그렇게 써도 되긴 하는데, 그렇게 하면 코드가 중복되고 나중에 유지보수 하기 힘들다)

  • custom hook

    function usePokemon() {
      return useQuery('pokemons', async () => {
        await new Promise(resolve => setTimeout(resolve, 1000))
        return axios
          .get('https://pokeapi.co/api/v2/pokemon')
          .then(res => res.data.results)
      })
    }
  • count.tsx

    function Count() {
      const queryInfo = usePokemon()
    
      return <h3>You are looking at {queryInfo.data?.length} pokemon</h3>
    }
  • Pocketmon.tsx

    function Pokemon() {
      const queryInfo = usePokemon()
    
      return queryInfo.isLoading ? (
        'Loading...'
      ) : queryInfo.isError ? (
        queryInfo.error.message
      ) : (
        <div>
          {queryInfo.data.map(result => {
            return <div key={result.name}>{result.name}</div>
          })}
          <br />
          {queryInfo.isFetching ? 'Updating...' : null}
        </div>
      )
    }

8. 병렬 쿼리(Paraller Query)

병렬로 쿼리 요청을 하기 위해서는 별다른 처리가 필요하지 않다. 그냥 쿼리를 나열해주면 된다.

  • 단, suspense 모드에서 사용할 경우 parallelism패턴이 동작하지 않으므로(첫 쿼리가 실행될 때 promise를 던지고 다른 쿼리가 실행되기 전까지 컴포넌트를 일시 중단한다) useQueries 를 사용하는 것을 추천한다.

예시코드

 function App () {
   // The following queries will execute in parallel
   const usersQuery = useQuery('users', fetchUsers)
   const teamsQuery = useQuery('teams', fetchTeams)
   const projectsQuery = useQuery('projects', fetchProjects)
   ...
 }

9. 쿼리에서 Props와 State 사용하기 (Using Props and State in Queries)

props와 state 사용

옵셔널 체이닝으로 유효한지 검사 후 props를 뿌려주면 된다.

... //상위 컴포넌트 
const [pokemon, setPokemon] = React.useState('')
...

function PokemonSearch({ pokemon }) {
  const queryInfo = useQuery(pokemon, async () => {
    await new Promise(resolve => setTimeout(resolve, 1000))
    return axios
      .get(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
      .then(res => res.data)
  })

  return queryInfo.isLoading ? (
    'Loading...'
  ) : queryInfo.isError ? (
    queryInfo.error.message
  ) : (
    <div>
      {queryInfo.data?.sprites?.front_default ? (
        <img src={queryInfo.data.sprites.front_default} alt="pokemon" />
      ) : (
        'Pokemon not found.'
      )}
      <br />
      {queryInfo.isFetching ? 'Updating...' : null}
    </div>
  )
}

10. 쿼리 비활성화 시키기(Disabling Queries)

위 예시와 같이 이런 식으로 요청을 보내면 초기 상태인 '' 값으로 요청이 가게 된다.

그것을 해결하려면 enabled 속성값을 추가해 pocketmon 이 있을 때만 실행되게 해주면 된다.

const queryInfo = useQuery(
    pokemon,
    async () => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return axios
        .get(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then(res => res.data)
    },
    {
      enabled: pokemon,
    }
  )

11. 다중 쿼리 키 (Multi-part Query Keys)

쿼리 키를 const queryInfo = useQuery(pokemon, async...) 라고 쓰면 onchange 로 검색할 때마다 pokemon 변수에 따라 새로운 key 가 생성되기 때문에 검색어가 쌓일수록 혼란스러워 진다.

export default function App() {
  const [pokemon, setPokemon] = React.useState('')
  return (
    <div>
      <input value={pokemon} onChange={e => setPokemon(e.target.value)} />
      <PokemonSearch pokemon={pokemon} />
      <ReactQueryDevtools />
    </div>
  )
}

function PokemonSearch({ pokemon }) {
  const queryInfo = useQuery(
    ['pokemon', pokemon], // AS IS : pokemon
    async () => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return axios
        .get(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then(res => res.data)
    },
    {
      enabled: pokemon,
    }
  )

이런식으로 된다.

['charizrd']
['pikachu']

그래서 array 를 사용해 prefix로 'pokemon'을 넣어주면 devtools에 조금 더 유기적으로 보인다..

['pokemon', 'charizrd']
['pokemon', 'pikachu']

array key 에 대한 내용은 이 블로그를 참고해 보자,, refetch할 때의 팁과, 쿼리키를 관리하는 방법까지 설명하는 블로그 글이다

https://tkdodo.eu/blog/effective-react-query-keys#structure

12. 자동 Automatic Query Retries

react-query는 요청이 실패하면 자동으로 3번 retry 한다

retry 에 재시도 할 횟수를 설정할 수 있다. false0 을 설정할 경우 retry 하지 않는다

retryDelay 를 설정해 재시도 할 때 delay 시간을 설정할 수 있다

const queryInfo = useQuery(
    ['pokemon', pokemon],
    async () => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return axios
        .get(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then(res => res.data)
    },
    {
      retry: 2,
      retryDelay: 1000,
      enabled: pokemon,
    }
  )

13. 쿼리 취소 Query Cancellation

[AbortController API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)fetch API 를 사용한다

  • promise가 resolve 되기 전에 언마운트되거나 사용되지 않는 쿼리는 취소되지 않는다.
  • promise가 resolve 된 후에 데이터를 캐시해서 사용할 수 있다.
function PokemonSearch({ pokemon }) {
  const queryInfo = useQuery(
    ['pokemon', pokemon],
    () => {
      const controller = new AbortController()

      const signal = controller.signal

      const promise = new Promise(resolve => setTimeout(resolve, 1000))
        .then(() => {
          return fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`, {
            method: 'get',
            signal,
          })
        })
        .then(res => res.json())

      promise.cancel = () => {
        controller.abort()
      }

      return promise
    },
    {
      enabled: pokemon,
    }
  )

14. 의존적인 쿼리 Dependent Queries

  • 한 쿼리가 다른 쿼리에 의존하는 경우 (userQuery의 값을 받아 → postQuery 순으로 요청을 해야할 때
  • isIdle은 enabled가 true이고 쿼리 시작할 때 true 이고 그 다음 isLoading단계로 이동한다.
  • isIdle이 아닌 isLoading으로 체크를 하게 되면 에러난다
import React from 'react'
import { useQuery } from 'react-query'
import { ReactQueryDevtools } from 'react-query-devtools'
import axios from 'axios'

const email = 'Sincere@april.biz'

function MyPosts() {
  const userQuery = useQuery('user', () =>
    axios
      .get(`https://jsonplaceholder.typicode.com/users?email=${email}`)
      .then(res => res.data[0])
  )

  const postsQuery = useQuery(
    'posts',
    () =>
      axios
        .get(
          `https://jsonplaceholder.typicode.com/posts?userId=${userQuery.data.id}`
        )
        .then(res => res.data),
    {
      enabled: userQuery.data?.id,
    }
  )

  return userQuery.isLoading ? (
    'Loading user...'
  ) : (
    <div>
      User Id: {userQuery.data.id}
      <br />
      <br />
      {postsQuery.isIdle ? null : postsQuery.isLoading ? (
        'Loading posts...'
      ) : (
        <div>Post Count: {postsQuery.data.length}</div>
      )}
    </div>
  )
}

15. Supplying a Query with Initial Data

initialData를 options을 설정하면 초기 데이터를 설정하고 초기 로딩 상태를 건너 뛸 수 있다

초기에 설정할 수 있는 데이터가 있을 때 사용

function Todos() {
   const result = useQuery('todos', () => fetch('/todos'), {
     initialData: initialTodos,
   })
 }

16. Marking Initial Query data as Stale

initialData 와 staleTime options을 설정하면 설정한 시간동안 refetch하지 않을 수 있다. (기본값은 0)

function Todos() {
   const result = useQuery('todos', () => fetch('/todos'), {
     initialData: initialTodos,
     staleTime: 1000,
   })
 }

list에 관련된 item 을 불러오는 쿼리

posts → post 를 불러온다고 예를 들었을 때

postId로 각각의 불러오기 때문에 postId로 complex key를 사용한다.

  const postQuery = useQuery(['post', postId], () => {
    return axios
      .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
      .then(res => res.data)
  })

18. Seeding Initial Query Data from Other Queries

posts 의 목록을 조회 한 후 id로 post를 다시 가지고 올 때 먼저 조회한 posts의 데이터를 queryCache에서 가지고 와서 initialData 로 설정할 수 있다.

queryCache.getQueryData('posts')

const postQuery = useQuery(
    ['post', postId],
    async () => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return axios
        .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
        .then(res => res.data)
    },
    {
      initialData: () =>
        queryCache.getQueryData('posts')?.find(post => post.id === postId),
    }
  )

19. Using Query Data to Seed Future Queries

나중에 쓸 데이터를 위해 캐싱하기

post의 목록을 조회하고 나중에 ['post', post.id] 로 조회하는 query를 위해 queryCachesetQueryData로 캐싱해 둔다

const postsQuery = useQuery('posts', async () => {
    await new Promise(resolve => setTimeout(resolve, 1000))
    const posts = await axios
      .get('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.data)

    posts.forEach(post => {
      queryCache.setQueryData(['post', post.id], post)
    })

    return posts
  })

20. Query Side-Effects

  • useQuery의 side-effect를 위한 callback option들이 있다.
  • onSuccess : 성공적으로 새 데이터를 가지고 올 때
  • onError : 에러일 때 실행
  • onSettled : 성공, 에러 둘다 실행, 성공시엔 errorundefined, 실패 시엔 dataundefined
const postsQuery = useQuery('posts', fetchPosts, {
    onSuccess: data => {
    ****},
    onError: error => {
		},
    onSettled: (data, error) => {
		},
  })

21. Scroll Restoration

  • Scroll Restoration: 뒤로가기를 누르면 이전에 방문한 페이지의 스크롤 위치로 가는 것, client side rendering이 되면서 퇴행했다고 한다.
  • react-query에서는 기본 값. 쿼리 결과가 캐시되고, 쿼리가 렌더링 될 때 동기적으로 찾을 수 있기 때문.
  • garbage 가 수집되지 않는 한 Scroll Restoration은 기본값이다.
  • garbage 수집 설정은 cacheTime으로 가능하고, 기본값은 5분이다
const postsQuery = useQuery('posts', fetchPosts, {
      cacheTime: 10000,
	})

22. Query Polling with Refetch Intervals

  • 일정 간격마다 서버에 요청하기
  • refetchInterval : 서버에 요청할 시간 간격
  • refetchIntervalInBackground : 열려있는 탭이 활성화 되어 있으라 때에도 요청할 건지 여부 기본값은 false
const timeQuery = useQuery(
    'posts',
    async () => {
      return axios.get('/api/time').then(res => res.data)
    },
    {
      refetchInterval: 1000,
      refetchIntervalInBackground: true,
    }
  )