원문 : https://tkdodo.eu/blog/mastering-mutations-in-react-query
리액트 쿼리 라이브러리 작업 대부분은 useQuery hook을 통한 데이터 검색에 관련한 것입니다.
그러나 데이터 작업에 두 번째로 필수적인 부분이 있습니다. 바로 업데이트입니다.
뮤테이션은 변형 > (서버 데이터 형태의 변경)으로 번역함. 4글자 다 적기도 귀찮고 의미도 애매한것 같아서...
변형(mutations)이 무엇인가요?
const myArray = [1]
myArray.push(2)
console.log(myArray) // [1, 2]
const myArray = [1]
const newArray = myArray.concat(2)
console.log(myArray) // [1]
console.log(newArray) // [1, 2]
useQuery와의 유사점
useMutation은 useQuery가 쿼리에 대해 수행하는 것처럼 변형 상태를 추적합니다.
loading, error 및 status 필드를 통해 사용자에게 무슨 일이 일어나고 있는지 쉽게 표시할 수 있습니다.
useQuery와의 차이점
useQuery는 선언적이며 useMutation은 명령형 스타일입니다.
React Query는 변형을 만들고 싶을 때마다 호출할 수 있는 함수를 제공합니다.
function AddComment({ id }) {
// this doesn't really do anything yet
const addComment = useMutation((newComment) =>
axios.post(`/posts/${id}/comments`, newComment)
)
return (
<form
onSubmit={(event) => {
event.preventDefault()
// ✅ mutation is invoked when the form is submitted
addComment.mutate(new FormData(event.currentTarget).get('comment'))
}}
>
<textarea name="comment" />
<button type="submit">Comment</button>
</form>
)
}
또 다른 차이점은 변형은 useQuery처럼 상태를 공유하지 않는다는 것입니다.
다른 컴포넌트에서 동일한 useQuery 호출을 여러 번 호출할 수 있으며 동일한 캐시된 결과를 반환할 수 있습니다.
그러나 이것은 변형에 대해서는 동작하지 않습니다.
쿼리와 뮤테이션 연결하기
변형은 설계에 따라 쿼리에 직접 연결되지 않습니다.
블로그 게시물을 "좋아요"하는 변형은 해당 블로그 게시물을 가져오는 쿼리와 관련이 없습니다.
이것이 동작하려면 React Query에 없는 일종의 기본 스키마가 필요합니다.
Invalidation
의도적으로 변형으로 서버 상태를 변경하는 시점은
캐시한 일부 데이터가 이제 "유효하지 않음"(invalid)을 React Query에 알리는 좋은 시점입니다.
React Query는 데이터가 현재 사용 중이면 해당 데이터를 다시 가져오고 가져오기가 완료되면 화면이 자동으로 업데이트됩니다.
라이브러리에 알려야 할 유일한 것은 무효화하려는 쿼리입니다.
const useAddComment = (id) => {
const queryClient = useQueryClient()
return useMutation(
(newComment) => axios.post(`/posts/${id}/comments`, newComment),
{
onSuccess: () => {
// ✅ refetch the comments list for our blog post
queryClient.invalidateQueries(['posts', id, 'comments'])
},
}
)
}
쿼리 무효화는 꽤 똑똑합니다.
모든 Query Filters와 마찬가지로 쿼리 키에 퍼지 일치를 사용합니다.
comment 목록에 대한 키가 여러 개인 경우 모두 무효화합니다.
그러나 현재 활성 상태(컴포넌트가 렌더링 중인)인 항목만 다시 가져옵니다.
나머지는 stale한 으로 표시되어 다음에 사용할 때 다시 가져옵니다.
예를 들어 코멘트를을 정렬할 수 있는 옵션이 있고,
새 코멘트가 추가되었을 때 캐시에 코멘트가 있는 두 개의 쿼리가 있다고 가정해 보겠습니다.
['posts', 5, 'comments', { sortBy: ['date', 'asc'] }
['posts', 5, 'comments', { sortBy: ['author', 'desc'] }
우리는 그 중 하나만 화면에 표시하기 때문에,
invalidateQueries는 하나를 다시 가져오고 다른 하나는 stale한 것으로 표시합니다.
직접 업데이트
const useUpdateTitle = (id) => {
const queryClient = useQueryClient()
return useMutation(
(newTitle) => axios.patch(`/posts/${id}`, { title: newTitle }),
{
// 💡 response of the mutation is passed to onSuccess
onSuccess: (newPost) => {
// ✅ update detail view directly
queryClient.setQueryData(['posts', id], newPost)
},
}
)
}
setQueryData를 통해 데이터를 캐시에 직접 넣으면 이 데이터가 백엔드에서 반환된 것처럼 동작하므로
해당 쿼리를 사용하는 모든 컴포넌트가 그에 따라 다시 렌더링됩니다.
전체 목록을 무효화하는 것이 "더 안전한" 접근 방식입니다.
저는 개인적으로 대부분의 경우 무효화가 우선되어야 한다고 생각합니다.
물론 사용 사례에 따라 다르지만 직접 업데이트가 안정적으로 작동하려면 프런트엔드에 더 많은 코드가 필요하고
백엔드에서 어느 정도 중복 논리가 필요합니다.
예를 들어 정렬된 목록은 업데이트로 인해 내 항목의 위치가 잠재적으로 변경되었을 수 있으므로 직접 업데이트하기가 매우 어렵습니다.
낙관적 업데이트
낙관적 업데이트는 React Query 변형의 주요 포인트 중 하나입니다.
useQuery 캐시는 특히 프리페치와 결합될 때 쿼리 간에 전환 즉시 데이터를 제공합니다.
덕분에 전체 UI가 매우 매끄럽게 느껴집니다.
낙관적 업데이트의 아이디어는 변형을 서버로 보내기 전에 변형의 성공을 속이는 것입니다.
성공적인 응답을 받으면 실제 데이터를 보기 위해 뷰를 다시 무효화하기만 하면 됩니다.
요청이 실패할 경우 UI를 변형 이전 상태로 롤백합니다.
이것은 즉각적인 사용자 피드백이 실제로 필요한 작은 변형에 적합합니다.
요청이 완료될 때까지 전혀 반응하지 않는 토글 버튼 만큼 나쁜 것은 없습니다.
사용자는 해당 버튼을 두 번 또는 세 번 클릭하면 모든 곳에서 "랙"이 느껴질 것입니다.
Example
예제는 official docs 와 in TypeScript 에서 확인하세요
- 추가하려는 todo의 id가 필요한 경우 어디서 얻나요?
- 현재 보고 있는 목록이 정렬된 경우 새 항목을 올바른 위치에 삽입할 수 있겠나요?
- 그 사이에 다른 사용자가 다른 것을 추가했다면 어떻게 할까요? --
- 다시 가져온 후 우리가 낙관적으로 추가한 항목이 위치를 전환할까요?
일반적인 문제들
awaited Promises
{
// 🎉 will wait for query invalidation to finish
onSuccess: () => {
return queryClient.invalidateQueries(['posts', id, 'comments'])
}
}
{
// 🚀 fire and forget - will not wait
onSuccess: () => {
queryClient.invalidateQueries(['posts', id, 'comments'])
}
}
Mutate or MutateAsync
const onSubmit = () => {
// ✅ accessing the response via onSuccess
myMutation.mutate(someData, {
onSuccess: (data) => history.push(data.url),
})
}
const onSubmit = async () => {
// 🚨 works, but is missing error handling
const data = await myMutation.mutateAsync(someData)
history.push(data.url)
}
const onSubmit = async () => {
// 😕 this is okay, but look at the verbosity
try {
const data = await myMutation.mutateAsync(someData)
history.push(data.url)
} catch (error) {
// do nothing
}
}
- 이것은 여러 변형을 동시에 시작하고 모두 완료될 때까지 기다리려는 경우
- 또는 콜백과 함께 콜백 지옥에 들어가는 종속 변형이 있는 경우에 필요할 수 있습니다.
변형은 변수 하나만 취합니다.
mutate의 마지막 인수는 옵션 객체이므로 useMutation은 현재 변수에 대해 하나의 인수만 사용할 수 있습니다.// 🚨 this is invalid syntax and will NOT work
const mutation = useMutation((title, body) => updateTodo(title, body))
mutation.mutate('hello', 'world')
// ✅ use an object for multiple variables
const mutation = useMutation(({ title, body }) => updateTodo(title, body))
mutation.mutate({ title: 'hello', body: 'world' })
이렇게 결정한 이유는 this discussion 을 살펴보세요
어떤 콜백은 호출되지 않습니다.
- useMutation 콜백에서 반드시 실행되어야 하는 로직과 관련된(예: 쿼리 무효화) 작업을 수행합니다.
- mutate 콜백에서 리디렉션 또는 알림 표시와 같은 UI 관련 작업을 수행합니다.
- 변형이 완료되기 전에 사용자가 현재 화면에서 멀어지면 의도적으로 실행되지 않습니다.
const useUpdateTodo = () =>
useMutation(updateTodo, {
// ✅ always invalidate the todo list
onSuccess: () => {
queryClient.invalidateQueries(['todos', 'list'])
},
})
// in the component
const updateTodo = useUpdateTodo()
updateTodo.mutate(
{ title: 'newTitle' },
// ✅ only redirect if we're still on the detail page
// when the mutation finishes
{ onSuccess: () => history.push('/todos') }
)
'FrontEnd' 카테고리의 다른 글
Intersection Observer와 React.lazy로 성능 개선하기 (0) | 2022.06.21 |
---|---|
리액트 쿼리 : 폼 (2) | 2022.06.20 |
리액트 쿼리 : 네트워크 오프라인 시 동작 (2) | 2022.06.20 |
리액트 쿼리 : 에러 처리 (0) | 2022.06.20 |
리액트 쿼리 : 상태 관리 라이브러리 (0) | 2022.06.19 |