♥️

useOptimistic に拡張性をもたせよう[カスタムフック: useOptimisticCount]

2023/10/15に公開

はじめに

ユーザーのフォロー・アンフォロー、投稿のお気に入り追加・削除などを行う際に 楽観的更新(optimistic updates) を用いることでUXを向上させます。
本記事では、react が提供している useOptimisticを用いて、カウントやアクティブ状態の管理に焦点を当てたカスタムフックuseOptimisticCountの作成過程とその機能について紹介します。

楽観的更新について

まず初めに、楽観的更新について説明します。
楽観的更新とは、アプリケーションのユーザーエクスペリエンス(UX)を高めるための技術的なアプローチです。これは、ユーザーにデータの更新が即座に行われたかのような感覚を提供する方法で、実際のデータの変更が確認される前にUIを更新します。

例えば、X のいいね機能に楽観的更新が使われています。X では、いいねのアイコンをタップした直後に色がつきますが、実はサーバへのリクエスト送信と UI 上のいいねの色の変化が同時に発生しています。つまり、サーバからの応答を待たずに UI を更新しています。

このようにして、ユーザーの操作を中断することなくスムーズな体験を続けられるので、全体的なUXが向上します。

ただし、楽観的更新は、操作が成功すると前提としてUIを先に更新するので、実際には失敗する場合も考慮して、適切なエラーハンドリングを行う必要があります。更新の失敗が頻発する場合、楽観的更新を用いることでユーザーの混乱や不信感を生じさせる可能性があるので、注意深く実装する必要があります。

カスタムフック:useOptimisticCount の実装方法

useOptimisticCountは、楽観的更新を活用したカスタムフックです。このセクションでは、その具体的な実装方法について説明します。

まず、フックの全体的なコードを以下に示します。

useOptimisticCount
import { experimental_useOptimistic as useOptimistic } from "react";

import { useToast } from "@/src/components/ui/use-toast";

import { ActionsResult } from "../types/ActionsResult";

type Props = {
  count: number;
  isActive: boolean;
  activeAction: (id: string) => Promise<ActionsResult>;
  inactiveAction: (id: string) => Promise<ActionsResult>;
};

export const useOptimisticCount = ({ count, isActive, activeAction, inactiveAction }: Props) => {
  const [optimisticState, setOptimisticState] = useOptimistic({ count, isActive });
  const { toast } = useToast();

  const updateCount = async (id: string) => {
    const action = isActive ? inactiveAction : activeAction;
    const newState = {
      count: optimisticState.count + (isActive ? -1 : 1),
      isActive: !optimisticState.isActive,
    };

    setOptimisticState(newState);

    try {
      const result = await action(id);
      if (!result.isSuccess) {
        toast({
          variant: "destructive",
          title: result.error,
        });
        setOptimisticState(optimisticState);
      }
    } catch (error) {
      console.error(error);
      setOptimisticState(optimisticState);
      toast({
        variant: "destructive",
        title: "エラーが発生しました🥲",
      });
    }
  };

  return {
    optimisticState,
    updateCount,
  };
};

Props の定義

type Props = {
  count: number;
  isActive: boolean;
  activeAction: (id: string) => Promise<ActionsResult>;
  inactiveAction: (id: string) => Promise<ActionsResult>;
};

上記のPropsで、このフックが受け取るパラメータを定義しています。それぞれのパラメータの説明は以下の通りです。

パラメータ 説明
count これは現在のカウント値を示すもので、例えば「いいね」の数などの数値を管理する際に使用します。
isActive このフラグは、何らかのトグル機能(例:フォロー中かどうか)の現在の状態を示します。
activeAction inactiveAction これらはアクティブ状態と非アクティブ状態を切り替える際に、サーバーサイドで実行される関数を指定するためのものです。具体的には、例えばユーザーをフォローする、またはフォローを解除するようなアクションをサーバーに伝える関数をここに設定します。

ロールバック処理も対応

エラーハンドリングも考慮しており、何か問題が発生した場合には楽観的更新を元に戻す(ロールバックする)対応も含めています。

try {
      const result = await action(id);
      if (!result.isSuccess) {
        toast({
          variant: "destructive",
          title: result.error,
        });
+     setOptimisticState(optimisticState);
      }
    } catch (error) {
      console.error(error);
+     setOptimisticState(optimisticState);
      toast({
        variant: "destructive",
        title: "エラーが発生しました🥲",
      });
    }

カスタムフック:useOptimisticCount の利用方法

あとは利用方法についてですが、Server Component で DB からデータを取得し、count と isActive に該当するデータやフラグを Client Component に渡します。
また activeAction と inactiveAction に関しては、Server Actions で定義されたサーバーサイドで動作する関数を指定します。

以下はユーザーのフォローに関する useOptimisticCount の呼び出し部分です。

const { optimisticState, updateCount } = useOptimisticToggle({
  count: followersCount,
  isActive,
  activeAction: followUser,
  inactiveAction: unFollowUser,
});

以下は実装例ですが、ボタンを押下すると、カウントが瞬時に更新されることが確認できます。

まとめ

useOptimisticCount は、ユーザーのフォローやアンフォロー、投稿のお気に入り追加・削除といったアクションに対するフィードバックを即座に提供することを目的としたカスタムフックです。このフックを使用することで、サーバーからの確定的なレスポンスを待たずに、一時的なUIの更新を行うことができます。結果として、ユーザーはアプリケーションの反応速度の向上を感じ、全体的な UX が向上します。また、楽観的更新を行う際には、エラーハンドリングの実装も重要であり、useOptimisticCount はその両方の側面を考慮して設計しています。
ぜひ、useOptimisticCount を試してみて、その効果を実感していただけると嬉しいです🙌

以上です!

Discussion