2022.07.06
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
どうも!フロントエンドエンジニアのわいです!
先月の中旬にチームを異動して、業務で初めてフロントエンドを担当しています。
ここ一か月弱 React を触ってきて少し慣れたので、今日はカスタムフックについて書いてみたいと思います。
こんなときに使いたくなるのか、カスタムフック
まだ React の勉強したての身でよくわかっていないので、正直カスタムフックについては「尖ったエンジニアが気持ち良くなるためのフック」でしかないと思っていました。
しかし、私にもついにカスタムフックを使いたくなるときが来ました…!
今回の要件は下記と想定します。
- コメント投稿用モーダルが一つ
- モーダルを開く発火点は複数
- コメントの作成とコメントの返信がある
- コメントへの返信の場合、別途返信先のコメントIDが必要
- コメント投稿後の挙動がモーダル発火点に応じて異なる
- 特定のページへ遷移
- ある Query を refetch
カスタムフックの作成
今回使用するライブラリは下記です。
- Next.js
- React
- Apollo Client
import { useState, useCallback, ChangeEvent, FormEvent } from 'react';
import { useMutation, MutationHookOptions } from '@apollo/client';
// ミューテーションの定義をインポート
import { CREATE_COMMENT, CREATE_REPLY_COMMENT } from '@/graphql/queries/comment';
export default function useCommentForm(postId: string) {
const [parentId, setParentId] = useState<string | undefined>();
const [commentMessage, setCommentMessage] = useState('');
const [commentFormIsOpen, setCommentFormIsOpen] = useState(false);
const [commentMutationOptions, setCommentMutationOptions] = useState<MutationHookOptions>();
const [commentErrorMessage, setCommentErrorMessage] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState(false);
// モーダルが開かれるときにオプションが更新される
const [createComment] = useMutation(CREATE_COMMENT, commentMutationOptions);
const [createReplyComment] = useMutation(CREATE_REPLY_COMMENT, commentMutationOptions);
const closeCommentForm = useCallback(() => {
setParentId(undefined);
setCommentMessage('');
setCommentMutationOptions(undefined);
setCommentErrorMessage(undefined);
setCommentFormIsOpen(false);
}, []);
// モーダルを開く関数の引数で、実行するミューテーション・ミューテーション後の挙動を制御する
const openCommentForm = useCallback(
(mutationOptions?: MutationHookOptions, parentId?: string) => {
setCommentMutationOptions({
onCompleted: () => {
closeCommentForm();
},
onError: (error) => {
setCommentErrorMessage('エラーが起きたよ!!');
},
...mutationOptions,
});
setParentId(parentId);
setCommentFormIsOpen(true);
},
[closeCommentForm]
);
const handleCommentMessageChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setCommentMessage(e.target.value);
}, []);
const handleSubmitComment = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 二重送信の防止
if (isLoading) return;
setIsLoading(true);
if (parentId) {
// コメントへの返信
await createReplyComment({
variables: {
input: {
parent_id: parentId,
comment: commentMessage,
},
},
});
} else {
// コメントの作成
await createComment({
variables: {
input: {
post_id: postId,
comment: commentMessage,
},
},
});
}
setIsLoading(false);
},
[postId, parentId, commentMessage, isLoading, createComment, createReplyComment]
);
return {
commentMessage,
commentFormIsOpen,
commentErrorMessage,
openCommentForm,
closeCommentForm,
handleCommentMessageChange,
handleSubmitComment,
};
}
MutationHookOptions
を引数で受け取ることで、refetchQueries
や onCompleted
が発火場所に応じて設定できるようになります。
カスタムフックの使用
簡易的に書くと、下記のようにコメントフォームを開くコンポーネントごとに、openCommentForm
を渡してあげることで実現します。
もちろん、カスタムフックなので他のページ(e.g. コメント返信一覧)でも同様に使用できます。
const Post: FC<Props> = ({ postId }) => {
// 省略
const {
commentMessage,
commentFormIsOpen,
commentErrorMessage,
openCommentForm,
closeCommentForm,
handleCommentMessageChange,
handleSubmitComment,
} = useCommentForm(postId);
return (
<>
{/* その他、コンポーネント */}
{/* PostDetailのボタンでコメントの新規作成 */}
<PostDetail
// その他Props
handleCommentBtnClick={useCallback(() => {
openCommentForm({
// 完了後、コメント一覧を更新する(refetch)
refetchQueries: [{ query: FETCH_COMMENTS_BY_POST_ID, variables: { post_id: postId } }],
});
}, [openCommentForm, postId])}
/>
{comments.map((comment) => (
<div key={comment.id}>
{/* 各コメントコンポーネントのボタンでコメントへ返信 */}
<Comment
// その他Props
handleCommentBtnClick={() => {
openCommentForm(
{
// 完了後、コメント返信一覧に遷移する
onCompleted: () => {
router.push(`/posts/${postId}/comments/${comment.id}`);
},
},
comment.id
);
}}
/>
</div>
))}
{commentFormIsOpen && (
// コメント投稿モーダル
<CommentFormModal
comment={commentMessage}
errorMessage={commentErrorMessage}
handleCancelBtnClick={closeCommentForm}
handleCommentChange={handleCommentMessageChange}
handleSubmit={handleSubmitComment}
/>
)}
</>
);
};
さいごに
自分がフロントエンド初心者のくせに、Apollo Client などのの基本的な使い方の説明を省いて申し訳ないですが、こんな感じで要件を満たせました!
まだまだ勉強中ですので、より良い方法があれば教えていただきたいです!
以上、わいでした。
健闘を祈る!!