React Query quick start


### 참고문헌
- [공식 홈페이지](https://tanstack.com/query/latest)
- [공식 레퍼런스 : Quick Start](https://tanstack.com/query/latest/docs/framework/react/quick-start)
## 설치
```
$ npm i @tanstack/react-query
$ npm i -D @tanstack/eslint-plugin-query
```

## 기본 세팅
- (주의사항) app router에서
	- root layout은 반드시 src/ 폴더의 root에 위치해야함. `()` 폴더를 사용하면 경로무시 폴더를 만들수 있는데 거기다가 layout 파일을 위치시키면 안됨
	- react-query가 적용된 페이지는 "use client"를 붙여줘야함.
## Queries
- [공식문서 : Queries](https://tanstack.com/query/latest/docs/framework/react/guides/queries) 
- 조회용 쿼리를 호출하는 경우 `useQuery` hook을 사용한다. 수정이 필요한 쿼리에도 사용할 수 있지만 그러한 경우 Mutations 를 사용한다. 
- useQuery를 사용하려면 key(queryKey)와 data 또는 error를 리턴하는 함수(queryFn)가 필요하다. 

```
import { useQuery } from '@tanstack/react-query'
function App() {  
	const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
```

- 위 예제에서 result는 다음의 유용한 변수들을 가지고 있다.
	- data : 쿼리 결과 데이터
	- status : 쿼리 상태
	- isPending : 쿼리 결과 데이터가 아직 없을 때. 쿼리는 호출 중 status === 'pending' 
	- isError : 에러인지 아닌지 체크 status === 'error'
	- isSuccess : 쿼리 결과가 성공했는지 체크 status === 'success'
	- isFetching : 위 3개의 조건은 내부적으로 'status'를 체크하지만 isFetching은 상태와 상관없이 쿼리가 실행중('fetching')이면 'true' 이다. 
- 구조분해 할당을 통해 아래와 같이 코딩이 가능하다.

```
function Todos() {  const { isPending, isError, data, error } = useQuery({    
	queryKey: ['todos'],    queryFn: fetchTodoList,  })
	
  if (isPending) {    return <span>Loading...</span>  }
  if (isError) {    return <span>Error: {error.message}</span>  }
  
  return (    
	  <ul>      
		  { data.map( (todo) => (<li key={todo.id}>{todo.title}</li>) ) }    
	  </ul> )
}
```
## Mutations
- [공식문서 : Mutations](https://tanstack.com/query/latest/docs/framework/react/guides/mutations) 
- 데이터의 추가, 수정, 삭제 시에는 `useMutation`을 사용한다.
- 데이터의 상태이상이 발생하므로 이에 대한 처리는 [[#Query Invalidation]] 항목을 을 참고

```
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>
  )
}
```

- 'Queries'에서와 마찬가지로 위 예제에서 'mutation' 상수는 다음의 프로퍼티를 가지고 있다.
	- isIdle : status === 'idle' , 리프레시, 리셋 등에 의한 초기 실행직전 상태이다.
	- isPending
	- isError
	- isSuccess
	- error
	- data
- mutation은 쿼리와 다르게 `mutation.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>
}
```

- use mutation 과 mutate의 차이

```
useMutation({
  mutationFn: addTodo,
  onSuccess: (data, error, variables, context) => {
    // Will be called 3 times
  },
})

const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
  mutate(todo, {
    onSuccess: (data, error, variables, context) => {
      // Will execute only once, for the last mutation (Todo 3),
      // regardless which mutation resolves first
    },
  })
})
```

- 리셋하기

```
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>
  )
}
```

- promises

``` 
const mutation = useMutation({ mutationFn: addTodo })

try {
  const todo = await mutation.mutateAsync(todo)
  console.log(todo)
} catch (error) {
  console.error(error)
} finally {
  console.log('done')
}
```

- retry

``` 
const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})
```

- Persist mutations
	- mutations은 필요할 때 재실행 될 수 있도록 저장될 수 있다. 이 기능은 hydrate function을 이용할 수 있다.
```
const queryClient = new QueryClient()

// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
  mutationFn: addTodo,
  onMutate: async (variables) => {
    // Cancel current queries for the todos list
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Create optimistic todo
    const optimisticTodo = { id: uuid(), title: variables.title }

    // Add optimistic todo to todos list
    queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])

    // Return context with the optimistic todo
    return { optimisticTodo }
  },
  onSuccess: (result, variables, context) => {
    // Replace optimistic todo in the todos list with the result
    queryClient.setQueryData(['todos'], (old) =>
      old.map((todo) =>
        todo.id === context.optimisticTodo.id ? result : todo,
      ),
    )
  },
  onError: (error, variables, context) => {
    // Remove optimistic todo from the todos list
    queryClient.setQueryData(['todos'], (old) =>
      old.filter((todo) => todo.id !== context.optimisticTodo.id),
    )
  },
  retry: 3,
})

// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })

// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)

// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)

// Resume the paused mutations:
queryClient.resumePausedMutations()
```
## Query Invalidation
- [공식문서 : Query Invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation) 
- 조회된 데이터가 변경되었을 때 변경된 데이터를 재 조회하는 것이 변경된 데이터임을 인지한 사용자에게 좋을 것이다. query invalidation을 이용하면 변경이후 데이터를 재 조회할 수 있게 해준다.
- 아래 두 예제에서 queryKey부분을 잘 살펴보자.

```
import { useQuery, useQueryClient } from '@tanstack/react-query'

// Get QueryClient from the context
const queryClient = useQueryClient()

queryClient.invalidateQueries({ queryKey: ['todos'] })

// Both queries below will be invalidated
const todoListQuery = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
  queryKey: ['todos', { page: 1 }],
  queryFn: fetchTodoList,
})
```

```
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})

// The query below will be invalidated
const todoListQuery = useQuery({
  queryKey: ['todos', { version: 20 }],
  queryFn: fetchTodoList,
})

// The query below will be invalidated
const todoListQuery = useQuery({
  queryKey: ['todos', { version: 10 }],
  queryFn: fetchTodoList,
})

// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
  queryKey: ['todos', { version: 5 }],
  queryFn: fetchTodoList,
})
```
## Devtools
- [공식 문서 : Devtools](https://tanstack.com/query/latest/docs/framework/react/devtools)
- Floating Mode

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

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

- in production
```
import * as React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { Example } from './Example'

const queryClient = new QueryClient()

const ReactQueryDevtoolsProduction = React.lazy(() =>
  import('@tanstack/react-query-devtools/build/modern/production.js').then(
    (d) => ({
      default: d.ReactQueryDevtools,
    }),
  ),
)

function App() {
  const [showDevtools, setShowDevtools] = React.useState(false)

  React.useEffect(() => {
    // @ts-expect-error
    window.toggleDevtools = () => setShowDevtools((old) => !old)
  }, [])

  return (
    <QueryClientProvider client={queryClient}>
      <Example />
      <ReactQueryDevtools initialIsOpen />
      {showDevtools && (
        <React.Suspense fallback={null}>
          <ReactQueryDevtoolsProduction />
        </React.Suspense>
      )}
    </QueryClientProvider>
  )
}

export default App
```

댓글

이 블로그의 인기 게시물

Session 대신 JWT를 사용하는 이유

VSCode에서의 VIM 단축키와 키보드 구매 가이드

우분투에서 테스트링크(testlink)와 맨티스(mantis)로 테스팅 서버 구성하기