ymmr
Recent Articles
    公開日: 2023/12/21

    POST でも宣言的に書きたい

    SWR はデータ取得のために使うライブラリという認識でいたのですが、useSWRMutation なるフックの存在を知りました。どうやら、SWR 2.0 から POST をはじめとした他の HTTP リクエストも任せられるようになった様です。今回は useSWRMutation でより宣言的なコードが書けるという話をします。

    宣言的なコードとは

    React が標榜している特徴に宣言的 (declarative) があります。説明のため、対義語にあたる命令的 (imperative) なコードから見ていきましょう。

    例えば、データ取得中はローディング UI を表示したいとします。愚直に実装すると、ローディング中であるかを判断するための状態が必要になります。

    const [isLoading, setIsLoading] = useState(false);
    
    async function fetchUsers() {
      // "ローディング中" という状態を設定する
      setIsLoading(true);
    
      // データ取得開始
      fetch('/api/users').then((response) => {
        const users = (await response.json()) as User[];
        setUsers(users);
    
        // "ローディング中" という状態を解除する
        setIsLoading(false);
      });
    }
    
    useEffect(() => {
      void fetchUsers();
    
      // クリーンアップなど...
    });
    

    命令的なプログラミングでは、プログラムの状態を何度も変化させることになります。その状態変化を上から順に実行することで目的を達成するため、処理の順番が重要になります。

    上記の例ではデータ取得が成功して (Promise の状態が fulfilled になって)から、ローディングの状態を変化させなければなりません。単純なコードでは問題になりませんが、コードが複雑化するにつれて本来行いたい処理を追うのが難しくなり、可読性が著しく悪化します。

    続いて、SWR を用いた宣言的なコードを見てみましょう。SWR がローディングの状態を隠蔽してくれるため、フックを呼び出してローディングの状態 isLoading を利用するだけです。

    // データを取得する関数
    const fetcher = (url: string) =>
      fetch(url).then(async (response) => {
        const users = (await response.json()) as User[];
    
        return users;
      });
    
    // データ取得をすると宣言するだけで、状態変化を伴わない
    const { data: users, isLoading } = useSWR('/api/users', fetcher);
    

    命令的なコードと比べて、宣言的なコードは処理の順番を意識せずに済みます。このように、何をしたいかを記述して、あとはよしなにやってくれという発想が宣言的なコードの特徴といえます。実装の詳細ではなくコンポーネントの持つ本来の機能に集中できるので、コードの可読性がとても高いです。

    useSWRMutation のインターフェース

    useSWRMutation は名前の通り、ミューテーション (create、update、delete) に適した設計となっています。useSWR と非常に似通ったインターフェースではありますが、いくつかの特徴的な違いがあります。

    • 任意のタイミングで fetcher を呼び出せる
    • fetcher の第 2 引数でミューテーションに使うデータを渡せる
    • リクエストに同じキー (e.g. /api/users) を使用すると、useSWR とキャッシュを共有できる
    const postRequest = (url: string, { arg }: { arg: Arguments }) => {
      //                               ^^^^^ fetcher は第 2 引数でデータを受け取れる
      return fetch(url, {
        method: 'POST',
        body: JSON.stringify(arg),
      }).then((response) => response.json());
    };
    const { trigger, isMutating } = useSWRMutation('/api/users', postUser);
    //       ^^^^^ trigger を呼び出すことで fetcher が実行される
    

    useSWRMutation を使った実装例

    ユーザーを追加する簡単な UI をつくってみました。

    useSWRMutation を以下のように活用しています。

    • ボタンをクリックしたタイミングで trigger を呼び出して、API を POST で叩く
    • POST のレスポンスを受け取るまでは、保存ボタンを無効にする
    • useSWR とキャッシュを同期することで、新規ユーザーをリストに追加する
    /pages/index.tsx
    import UserList from '@/components/UserList';
    import { useState } from 'react';
    import useSWRMutation from 'swr/mutation';
    
    import type { Arguments } from 'swr';
    
    const Page = () => {
      const defaultValue = '';
      const [inputValue, setInputValue] = useState(defaultValue);
    
      const postRequest = (url: string, { arg }: { arg: Arguments }) => {
        return fetch(url, {
          method: 'POST',
          body: JSON.stringify(arg),
        }).then((response) => response.json());
      };
      const { trigger, isMutating } = useSWRMutation('/api/users', postRequest);
    
      // POST のレスポンスを受け取るまでは、保存ボタンを無効にする
      const canSubmit = !isMutating && inputValue !== defaultValue;
     
      return (
        <>
          <input
            type="text"
            value={inputValue}
            placeholder="New username"
            onChange={(e) => setInputValue(e.target.value)}
          />
          <button
            disabled={!canSubmit}
            type="button"
            onClick={() => {
              // ボタンをクリックしたタイミングで API を POST で叩く
              void trigger({ name: inputValue });
            }}
          >
            Save
          </button>
          {/* useSWR でデータを取得をしているコンポーネント */}
          <UserList />
        </>
      );
    };
    
    export default Page;
    
    
    /components/UserList.tsx
    import useSWR from 'swr';
    
    import type { User } from '@/types/User';
    
    const UserList = () => {
      const { data, isLoading } = useSWR<{ users: User[] }, Error>(
        '/api/users',
        // ^^^^^ useSWRMutation とキーを共有する。
        async (url: string) => {
          const response = await fetch(url);
          const data = (await response.json()) as { users: User[] };
    
          return { users: data.users };
        },
      );
    
      if (isLoading) {
        return <div>Loading...</div>;
      }
    
      return (
        <ul>
          {data && data.users.map((user) => <li key={user.id}>{user.name}</li>)}
        </ul>
      );
    };
    
    export default UserList;
    

    おわりに

    これまでデータ取得は SWR、POST 等は Axios と使い分けていたので、もっと早く useSWRMutation の存在を知りたかったです。将来的には React Suspense と組み合わせて、より宣言的なコードが書ける様になることを期待しています。