useMutation
TanStack Query로 데이터를 가져오는 것 뿐만 아니라 새 이벤트 생성같이 데이터를 전송하는 것도 가능하다.
폼을 제출해 데이터를 전송하는 예제를 통해서 데이터를 전송하고 수집해보자!
먼저 아래와 같은 api를 http.js에 추가해준다.
export async function createNewEvent(eventData) {
const response = await fetch(`http://localhost:3000/events`, {
method: 'POST',
body: JSON.stringify(eventData),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = new Error('An error occurred while creating the event');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { event } = await response.json();
return event;
}
이전에 useQuery를 쓸 때 queryKey, queryFn을 사용했던 것과 유사하게 useMutation이라는 훅을 사용할 것이다.
useMutation에서는 mutationKey, mutationFn으로 쓰이는데, 전자의 경우 반드시 필요한 값은 아니다.
그 이유는 key가 응답 데이터를 캐싱할 때 사용되는데 변형의 목적은 캐시 처리가 아니라 변경을 적용하는 것이기 때문이다.
useMuation 역시 구성 객체가 필요하기 때문에 객체 안에 mutationFn을 넣어주고, 위에서 작성한 createNewEvent 함수를 적는다.
그리고 객체를 반환하기에 객체 구조 분해로 필요한 프로퍼티에 접근한다.
mutate, isPending, isError, error를 가져와 각 상황에 맞는 UI를 보여준다.
handleSubmit 함수는 mutate를 호출해야 하고 이 경우에는 formData를 전달해야 한다.
백엔드 코드에서 객체의 event 프로퍼티는 formData 값으로 사용하기로 해서 이렇게 작성했지만 실제로는 백엔드 데이터 형식에 맞춰 작성하면 된다.
요지는 mutate 함수 안에 요청을 보낼 데이터를 넣어주면 된다는 것이다.
export default function NewEvent() {
const navigate = useNavigate();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createNewEvent,
});
function handleSubmit(formData) {
mutate({ event: formData });
}
return (
<Modal onClose={() => navigate('../')}>
<EventForm onSubmit={handleSubmit}>
{isPending && 'Submitting...'}
{!isPending && (
<>
<Link to="../" className="button-text">
Cancel
</Link>
<button type="submit" className="button">
Create
</button>
</>
)}
</EventForm>
{isError && (
<ErrorBlock
title="Failed to create event"
message={
error.info?.message ||
'Failed to create event. Please check your inputs and try again later.'
}
/>
)}
</Modal>
);
}
그런데 이렇게 하면 예제의 로직 상 폼을 제출할 때 이미지를 선택하지 않았기 때문에 400번 에러가 발생한다.
폼을 작성할 때 이미지도 선택할 수 있게 코드를 수정해보자.
폼을 만들 때 이미지를 선택해야 하니 EventForm 컴포넌트로 가 이미지 리스트를 표시해준다.
EventForm 컴포넌트를 보면 이런 식으로 구성되어 있는데, ImagePicker가 빈 배열로 되어 있다.
function handleSelectImage(image) {
setSelectedImage(image);
}
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData);
onSubmit({ ...data, image: selectedImage });
}
return (
<form id="event-form" onSubmit={handleSubmit}>
<p className="control">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
defaultValue={inputData?.title ?? ''}
/>
</p>
<div className="control">
<ImagePicker
images={[]}
onSelect={handleSelectImage}
selectedImage={selectedImage}
/>
</div>
이게 아니라 useQuery로 이미지 리스트를 가지고 오고 그 데이터를 ImagePicker의 images로 넣어주자.
이를 위해선 useQuery 훅을 사용해야 한다.
const { data, isPending, isError } = useQuery({
queryFn: fetchSelectableImages,
queryKey: ['events-images'],
});
이 때 queryFn은 이미 준비해둔 함수 fetchSelectableImages를 사용하면 된다.
이전에 사용했던 signal 객체로 유저가 페이지를 떠나거나 네트워크가 중단될 경우 요청을 하지 않게 한다.
export async function fetchSelectableImages({ signal }) {
const response = await fetch(`http://localhost:3000/events/images`, {
signal,
});
if (!response.ok) {
const error = new Error('An error occurred while fetching the images');
error.code = response.status;
error.info = await response.json();
throw error;
}
const { images } = await response.json();
return images;
}
useQuery로 이미지 데이터를 받아올 때도 이전과 마찬가지로 isPending, isError, error, data를 사용해 UI를 구성한다.
{isPending && <p>Loading selectable images...</p>}
{isError && (
<ErrorBlock
title="Failed to load selectable images"
message="Please try again later."
/>
)}
{data && (
<div className="control">
<ImagePicker
images={data}
onSelect={handleSelectImage}
selectedImage={selectedImage}
/>
</div>
)}
이제 폼을 작성하고 제출할 때 이미지까지 선택하여 400 에러는 더 이상 뜨지 않는다.
하지만 새로운 데이터를 만들어 전송했을 때, 아무런 반응이 일어나지 않는다.
이는 변형에 성공했을 때의 동작에 대해 아무런 코드도 작성하지 않았기 때문이다.
그러니까 변형은 성공했는데 성공한 후에 어디로 navigate 하든가 alert이 뜨든가 어떤 액션을 해 유저에게 '니 데이터 전송됐음'을 알려줘야 하는데 그런 게 없기 때문에 그냥 덩그러니 ㅇ.ㅇ 있는 것이다.
변형 성공 시 동작
변형이 성공했을 때 어떤 동작을 취하게 하려면 useMutation 구성 객체에 onSuccess 프로퍼티를 추가하면 된다.
이 프로퍼티는 함수를 값으로 사용한다.
export default function NewEvent() {
const navigate = useNavigate();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createNewEvent,
onSuccess: () => {
navigate('/events');
}, // 함수를 값으로 씀
});
이런 식으로 새 이벤트를 만들어 변형에 성공할 경우 /events로 이동하도록 코드를 작성했다.
이렇게 onSuccess에 함수를 작성해야만 '성공'할 경우 해당 함수가 실행되며 실패하면 isError 구문으로 내려가게 된다.
하지만 이렇게 변형을 시도하고, 성공하고, /events로 이동해도 방금 막 추가한 이벤트가 이벤트 목록에 로드되지 않는다.
새로고침을 하거나 페이지를 이동했다 돌아와야만 보여진다.
폼을 제출해 데이터가 변경된 게 확실한 경우에 바로 데이터를 다시 가져오게 업데이트를 시키고 싶다.
변형 성공 시 쿼리 무효화
업데이트를 시키려면 이전에 가져온 데이터가 만료됐다고 표시하고 다시 데이터를 가져오게 트리거 해야 한다.
이를 위해 쿼리에서 제공하는 메소드를 사용해 하나 이상의 쿼리를 무효화 할 수 있다.
한 마디로 '니 데이터 오래된 거니까 새로 가져오셈'을 위해 쿼리를 무효화 시키는 것이다.
queryClient.invaildateQueries를 사용하면 되는데, 현재 queryClient를 App.jsx에서 import 했기 때문에, 이를 다른 곳에서도 쓰기 위해 http.js에 해당 부분을 붙여 넣고 사용할 컴포넌트에서 import하기로 한다.
export default function NewEvent() {
const navigate = useNavigate();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createNewEvent,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] }); // queryKey를 포함한 쿼리 무효화
navigate('/events');
}, // 함수를 값으로 씀
});
NewEvent에서 queryClient를 import하고, 무효화 할 쿼리의 키를 적어준다.
이 때 queryKey에는 해당 키를 포함한 쿼리들이 무효화 된다.
만약 정확히 특정 키만 무효화하려면 exact: true를 넣어주면 된다.
현재는 다른 컴포넌트들에도 events 쿼리 키가 사용되고 있으므로 그곳들에서도 새 데이터를 적용하기 위해 exact를 넣지 않았다.
이제 데이터가 성공적으로 변형되면 바로 반영되고, /events로 이동된다.
'🌺 TanStack Query' 카테고리의 다른 글
[TanStack Query] Type has no properties in common with type invalidateQueries 에러 해결 (2) | 2024.06.20 |
---|---|
[TanStack Query] 낙관적 업데이트 (0) | 2024.05.07 |
[TanStack Query] 동적 쿼리와 enabled를 사용한 쿼리 활성/비활성화 (0) | 2024.04.30 |
[TanStack Query] useQuery 사용하기 (0) | 2024.04.30 |
[TanStack Query] TanStack Query(React Query) 이해하기 (1) | 2024.04.28 |