react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용된다.
function Todos() {
const { status, data, error } = useQuery("todos", fetchTodoList);
// isLoading, isError로 할 필요 없이 status로도 가능하다
if (status === "loading") {
return <span>Loading...</span>;
}
if (status === "error") {
return <span>Error: {error.message}</span>;
}
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
"nextTodos",
fetchNextTodoList,
{
enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
}
);
이럴 때 useQueries를 사용하면 여러개의 useQuery를 하나로 묶을 수 있다.
useQueries를 사용하면 promise.all과 마찬가지로 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다.
const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);
// 어짜피 세 함수 모두 비동기로 실행하는데, 세 변수를 개발자는 다 기억해야하고 세 변수에 대한 로딩, 성공, 실패처리를 모두 해야한다.
// useQueries를 사용하면 여러개의 쿼리를 한번에 받아올 수 있다.
const result = useQueries([
{
queryKey: ["users"],
queryFn: () => api.fetchUsers()
},
{
queryKey: ["teams"],
queryFn: () => api.fetchUsers()
}
]);
값을 바꿀때 사용하는 api. return 값은 useQuery와 동일
mutationFn은 mutation Function으로 promise 처리가 이루어지는 함수, 다른 말로는 axios를 이용해 서버에 API를 요청하는 부분
const editMutation = useMutation(editProfileImg)
mutate는 useMutation을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할
즉, useMutation을 정의 해둔 뒤 이벤트가 발생되었을 때 mutate를 사용해주면 된다.
요청이 성공, 실패했을 경우에 실행할 함수를 선언할 수 있다.
onSettled를 통해서도 가능하다.
const savePerson = useMutation((person: Iperson) => axios.post('http://localhost:8080/savePerson', person), {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
});
아래는 프로젝트에서 사용한 프로필 수정 예제다
const queryClient = useQueryClient()
const imgRef: any = useRef<HTMLInputElement | null>(null)
const [alertMessage, setAlertMessage] = useState('')
const [showCustomAlert, setShowCustomAlert] = useState<boolean>(false)
const [imgUrl, setImgUrl] = useState<string | ArrayBuffer | null>()
const [imgFile, setImgFile] = useState<File>()
const [newNickname, setNewNickname] = useState('')
const editMutation = useMutation(editProfileImg, {
onSuccess: () => {
queryClient.invalidateQueries()
},
})
const changeMutation = useMutation(changeNickname, {
onSuccess: () => {
queryClient.invalidateQueries()
},
})
const onChangeImageHandler = () => {
const reader = new FileReader()
const file = imgRef.current.files[0]
if (file.size > 5 * 1024 * 1024) {
setAlertMessage('파일 크기는 최대 5MB 입니다')
setShowCustomAlert(true)
setImgUrl(null)
} else {
reader.readAsDataURL(file)
reader.onloadend = () => {
setImgUrl(reader.result)
setImgFile(file)
}
}
}
const onSubmitImageHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!imgFile) {
setAlertMessage('먼저 변경할 프로필 이미지를 등록해 주세요.')
setShowCustomAlert(true)
} else {
e.preventDefault()
const data = new FormData()
data.append('image', imgFile as File)
editMutation.mutate(data)
onSetLocalStorageHandler('img', imgUrl)
setAlertMessage('변경되었습니다.')
setShowCustomAlert(true)
}
}
const onChangeNicknameHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
let regExp: RegExp = /^[^\s]{2,8}$/
if (regExp.test(newNickname) && newNickname !== '') {
checkNickname(newNickname)
.then(() => {
changeMutation.mutate(newNickname)
setAlertMessage('변경되었습니다.')
setShowCustomAlert(true)
setNewNickname('')
onSetLocalStorageHandler('nickname', newNickname)
})
.catch((error) => {
setAlertMessage(error.response.data.message)
})
} else if (regExp.test(newNickname) === false) {
setAlertMessage('닉네임은 2자리 이상, 8자리 이하여야 합니다.')
setShowCustomAlert(true)
}
}
if (profLoading) return <p></p>
return (
<>
<CustomAlert
showAlert={showCustomAlert}
onHide={() => setShowCustomAlert(false)}
message={alertMessage}
loginState={false}
/>
<MyPageContentsContainer>
<ExternalContainer>
<MyPageEditContainer>
<label htmlFor="fileinput">
<EditDiv>
<BsPlusLg />
</EditDiv>
</label>
<MyPageEditImg
src={
!imgUrl
? profData.profileUrl
? profData.profileUrl
: baseProifle
: imgUrl
}
alt="이미지"
/>
<MyPageImgEditInput
id="fileinput"
ref={imgRef}
type="file"
accept="image/*"
onChange={onChangeImageHandler}
/>
<MyPageImgBtnWrap>
<MyPageInputBtn onClick={onSubmitImageHandler}>
수정하기
</MyPageInputBtn>
</MyPageImgBtnWrap>
<MyPageInputContainer>
<form>
<MyPageInputLabel>닉네임</MyPageInputLabel>
<MyPageInput
type="text"
value={newNickname}
placeholder="닉네임을 입력하세요"
onChange={(e) => setNewNickname(e.target.value)}
/>
<MyPageEditBtnTwo
type="submit"
onClick={onChangeNicknameHandler}
>
닉네임 변경
</MyPageEditBtnTwo>
</form>
</MyPageInputContainer>
</MyPageEditContainer>
</ExternalContainer>
</MyPageContentsContainer>
</>
)
}
invalidateQueries는 useQuery에서 사용되는 queryKey의 유효성을 제거해주는 목적으로 사용된다.
그리고 queryKey의 유효성을 제거해주는 이유는 서버로부터 다시 데이터를 조회해오기 위함
useQuery에는 staleTime과 cacheTime이 존재한다. 정해진 시간이 지나지 않으면, 새로운 데이터가 추가되어도 useQuery는 변동 없이 동일한 데이터를 보여준다.
사용자 입장에서는 데이터 생성이 제대로 되었는지에 대한 파악이 힘들기 때문에 혼란을 겪을 수 있게 된다.
이걸 해결하기 위해 사용하는 invalidateQueries이다.
데이터를 저장할 때 invalidateQueries를 이용해 useQuery가 가지고 있던 queryKey의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청
const editMutation = useMutation(editProfileImg, {
onSuccess: () => {
queryClient.invalidateQueries()
},
})
const changeMutation = useMutation(changeNickname, {
onSuccess: () => {
queryClient.invalidateQueries()
},
})