公開日:2022.07.06

【Apollo Client】Mutation後の挙動を呼び出し箇所に応じて変更するカスタムフック

テクログjavascript

どうも!フロントエンドエンジニアのわいです!

先月の中旬にチームを異動して、業務で初めてフロントエンドを担当しています。

ここ一か月弱 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 を引数で受け取ることで、refetchQueriesonCompleted が発火場所に応じて設定できるようになります。

カスタムフックの使用

簡易的に書くと、下記のようにコメントフォームを開くコンポーネントごとに、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 などのの基本的な使い方の説明を省いて申し訳ないですが、こんな感じで要件を満たせました!

まだまだ勉強中ですので、より良い方法があれば教えていただきたいです!

以上、わいでした。

健闘を祈る!!

この記事を書いた人

わい

入社年2019年

出身地大阪

業務内容システム開発

特技または趣味芸人のラジオを聴く、ダイビング

わいの記事一覧へ

テクログに関する記事一覧