Custom hook은 컴포넌트 로직을 함수로 뽑아내어 재사용하기 위해 사용한다.
hook은 항상 컴포넌트의 최상위 코드에서만 호출되어야 한다. 이 규칙을 따라야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것을 보장 할 수 있다.
Custom hook을 만들때 이름은 use로 시작해야한다. 이를 따르지 않으면 특정한 함수가 그 안에서 Hook을 호출하는지를 알 수 없기 때문에 Hook 규칙의 위반 여부를 자동으로 체크할 수 없다.
useQuery는 서버에서 데이터를 가져오는데 사용하는 함수이다. queryFn과 queryKey라는 두가지 파라미터가 꼭 필요하다.
queryFn을 통해 서버에서 데이터를 가져오고, 이를 query Key값으로 저장하여 애플리케이션 내부에서 다시 가져오고, 캐싱하고 공유 할 수 있다.
query Key는 배열이여야 한다. string값이 query Key로 주어져도 내부적으로 길이가 1인 배열로 변환된다.
//useCommentQuery.ts
import * as API from '../../api/API';
import { useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Tcomment } from '../../type/commentType';
const fetchComments = async (shopId: number) => {
return await API.get(`/api/comments?shopId=${shopId}`);
};
export const useCommentQuery = (shopId: number) => {
const {
isLoading: isCommentLoading,
isError: isCommentError,
data: commentState,
isSuccess,
} = useQuery<Tcomment[], AxiosError>(['comment', shopId], () => fetchComments(shopId));
return { isCommentLoading, isCommentError, commentState, isSuccess };
};
Custom hook으로 만들때는 먼저 쿼리별로 재사용을 하도록 파라미터와 에러타입 명시와 쿼리별로 hook을 작성하였고
반환값 네이밍을 쿼리별로 다르게 해 사용하는 컴포넌트에서의 코드를 명확하게 했다.
// foodDetail.tsx
const FoodDetail = () => {
const scrollRef = useRef<HTMLElement>(null);
const shopId = Number(useParams().id);
const { isCommentLoading, isCommentError, commentState } = useCommentQuery(shopId);
const { isMenuLoading, isMenuError, menuState } = useMenuQuery(shopId);
const { isShopLoading, isShopError, shopState } = useShopQuery(shopId);
if (isCommentLoading || isMenuLoading || isShopLoading) {
return <S.CommentContainer>로딩중</S.CommentContainer>;
}
if (isCommentError || isMenuError || isShopError) {
return <S.CommentContainer>Error 발생</S.CommentContainer>;
}
return (
<S.Pagecontainer ref={scrollRef}>
<NavBar />
<DetailSlider imageArr={makeImgArr(shopState, menuState)} />
<Content shop={shopState} />
<Comment shopId={shopState?.shopId} scrollRef={scrollRef} />
<S.CommentContainer>
{commentState?.map((comment) => (
<CommentList key={comment.commentId} commentProp={comment} shopId={shopId} />
))}
</S.CommentContainer>
<Footer />
</S.Pagecontainer>
);
};
export default FoodDetail;
쿼리의 상태가 로딩중이거나 에러가 존재할 수 있으므로, 이를 타입가드를 해주어야 데이터가 undefined 되지 않는다.
hook에서 각 쿼리별 상태를 받은 후 상태에 따라 타입가드를 진행했다.
const isLoading = commentLoading || menuLoading || shopLoading
// isLoading을 사용하면 타입가드가 제대로 이루어지지 않음.
공식문서에서는 서버 데이터를 직접 변경할 경우 useQuery 대신 useMutation을 사용하는 것을 권장한다.
mutation에서는 api호출이 성공했을때 onSuccess라는 콜백함수를 호출하고, 에러가 발생했을때는 onError라는 콜백함수를 호출한다.
mutation Fn은 오직 하나의 오직 하나의 변수만 파라미터로 받을 수 있다. 여러개의 파라미터를 넘겨줄때는 객체형태로 넘겨줘야 한다.
오직 하나의 파라미터만 받을 수 있는 이유는 Mutation Function은 Type으로 TVariable이라는 하나의 파라미터만 받을 수 있도록 정의되어 있다.
export type UseMutateFunction<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown,
> = (
...args: Parameters<MutateFunction<TData, TError, TVariables, TContext>>
) => void
//usePatchComment.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as API from '../../api/API';
import { PostComment } from '../../type/commentType';
type PatchComment = {
comment: PostComment;
commentId: number;
};
const patchComment = async ({ comment, commentId }: PatchComment) => {
return await API.patch(`/api/comments/${commentId}`, comment);
}; // mutation function은 오직 하나의 variable or object만 파라미터로 받을 수 있다.
type UsePatchComment = {
onSuccessCb: () => void;
onErrorCb: () => void;
shopId: number | undefined;
};
export const usePatchComment = ({ onSuccessCb, onErrorCb, shopId }: UsePatchComment) => {
const queryClient = useQueryClient();
const mutation = useMutation(patchComment, {
onSuccess: () => {
onSuccessCb();
queryClient.invalidateQueries(['comment', shopId], { exact: true });
},
onError: () => {
onErrorCb();
},
});
return { mutation }; // mutation객체 반환
};
//TextArea.tsx
interface TextAreaProps {
commentId: number;
shopId: number;
commentStar: NullableNum;
content: string;
canRevise: boolean;
updateRevise: (x: boolean) => void;
updateReadOnly: (x: boolean) => void;
}
const TextArea = ({
commentId,
shopId,
commentStar,
content,
canRevise,
updateRevise,
updateReadOnly,
}: TextAreaProps) => {
const [textValue, setTextValue] = useState<string>(content);
const onSuccessCb = () => {
updateRevise(false);
updateReadOnly(true);
}; //요청이 성공시 호출됨
const onErrorCb = () => {
alert('요청에 실패하였습니다.');
}; // 요청 실패시 호출됨
const { mutation: patchComment } = usePatchComment({ onSuccessCb, onErrorCb, shopId });
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTextValue(e.target.value);
};
const reviseEnd = (e: React.MouseEvent<HTMLButtonElement>) => {
if (inValidateText(textValue, commentStar)) return;
const revisedComment = {
star: commentStar,
content: textValue,
};
patchComment.mutate({ comment: reviseComment, commentId });
}; // 이벤트핸들러에서 유효한 데이터라면 hook 실행됨.
return (
<S.TextContainer>
<S.CommentArea
value={textValue}
onChange={handleChange}
disabled={!canRevise}
maxLength={100}
/>
{canRevise && <S.Button onClick={reviseEnd}>수정완료</S.Button>}
</S.TextContainer>
);
};
ref)