🫶

Next.js + Typescript + axiosでリリースされたばかりのSWR2.0のuseSWRMutationを試した

2023/02/08に公開

はじめに

今まではAPIでGETする時はSWRでやるものの、PUT/POST/DELETE等はaxiosとかの別ライブラリを使うといった技術選定が多かったように思えます。

今回リリースされたSWR 2.0が新たに提供するuseSWRMutationによって、いよいよPUT/POST/DELETEも全部SWRで出来ちゃうよって感じになってきた気がします。
https://swr.vercel.app/blog/swr-v2

SWR 2.0 では、新しいフック useSWRMutation によって、
宣言的な API を使用してリモートでデータを変更することがより簡単になりました。
このフックを使って変異をセットアップし、後でそれを有効にすることができます。

公式ドキュメントにはこのように書かれてるのですが、typescriptのサンプルコードがなかったので自分で作ってみました。
https://swr.vercel.app/blog/swr-v2#useswrmutation

新規ユーザー作成(POST API)

カスタムフック

サンプルで新規ユーザー登録フォームを書いてみました。
useSWRMutationを内包するカスタムフックを作成

useCreateUser.ts
// Fetcher implementation.
// The extra argument will be passed via the `arg` property of the 2nd parameter.
// In the example below, `arg` will be `'my_token'`
// Return must return Promise
type Arg = {
  arg: Arguments; // assigned user requrst body
};
const createUser = async (url: string, { arg }: Arg) => {
  // async関数を使う事でawait構文が利用可能になる。
  // await構文の効果は、Promiseインスタンスの.then(val => {})を実行してvalを取り出す
  console.log('arg', arg);

  const res = await axios.post(url, arg); // assign arg to request body
  console.log('res ', res.data);

  // Promiseインスタンスでres.dataを返すように変更
  return res.data;
};

type User = IFormInput;
const useCreateUser = () => {
  const { trigger, isMutating, data, error } = useSWRMutation<
    AxiosResponse<User>,
    AxiosError
  >(`/user`, createUser);

  // debug
  console.log('data', data);
  console.log('error', error);

  return { trigger, isMutating, data, error };
};

featureの説明

公式ドキュメントではこのようにfeatureが使われてますが

async function sendRequest(url, { arg }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  })
}

axiosだとこんな感じになります。

// Fetcher implementation.
// The extra argument will be passed via the `arg` property of the 2nd parameter.
// In the example below, `arg` will be `'my_token'`
// Return must return Promise
type Arg = {
  arg: Arguments; // assigned user requrst body
};
const createUser = async (url: string, { arg }: Arg) => {
  // async関数を使う事でawait構文が利用可能になる。
  // await構文の効果は、Promiseインスタンスの.then(val => {})を実行してvalを取り出す
  console.log('arg', arg);

  const res = await axios.post(url, arg); // assign arg to request body
  console.log('res ', res.data);

  // Promiseインスタンスでres.dataを返すように変更
  return res.data;
};

argにSWRが提供してるArgumentsという型を割り当てることが出来ます。(anyは良くない)

import { Arguments } from 'swr';
const fetcher = (url: string, { arg }: { arg: Arguments }) =>

以下の書き方のほうがさらに型を強くすることができるそうです。
引用元:https://zenn.dev/riya_amemiya/articles/6910d97b81e917#comment-801e5cd58b584d

const fetcher = <
  T extends {
    [key: string]: unknown
  }
>(
  url: string,
  {
    arg,
  }: {
    arg: {
      query: string
      variables: T
    }
  },
) =>
  fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg),
  }).then((r) => r.json())

useSWRMutationの箇所

公式ドキュメントではこのような書き方ですが、これはjavascriptなのでtypescriptだと少し違います。

  const { trigger, isMutating } = useSWRMutation('/user', createUser)

以下のように型を割り当てます。

 type User = {
     name: string,
     age: string
 };
 const { trigger, isMutating, data, error } = useSWRMutation<
    AxiosResponse<User>,
    AxiosError
  >(`/user`, createUser);

ハマったところ

理解が浅いのであってるか自信ないのですが、
axios.then〜catch構文だとPromiseインスタンスでres.dataを返さないので正しく動作しません。

axios.get('/user?ID=12345')
.then(function (response) {
    // handle success
  console.log(response);
})
.catch(function (error) {
    // handle error
  console.log(error);
})

async/awaitでtry catchを使うか
もしくは私と同じように、async/awaitで定数resで受け取って返す必要あります。

const createUser = async (url: string, { arg }: Arg) => {
  // async関数を使う事でawait構文が利用可能になる。
  // await構文の効果は、Promiseインスタンスの.then(val => {})を実行してvalを取り出す
  console.log('arg', arg);

  const res = await axios.post(url, arg); // assign arg to request body

呼び出し元の新規ユーザー作成フォーム

index.tsxでカスタムフックに内包したuseSWRMutaionを呼び出してます。
以下のような流れで処理が走ります。

  1. formに新規ユーザーの情報(名前・年齢等)を入力しsubmitボタンを押す
  2. onSubmit関数が走り、内部にあるtrigger(data)を経由
  3. カスタムフック内のuseSWRMutaionを経由
  4. axiosでAPIにPOST(なんだかややこしいですね😥)
index.tsx
  const { trigger, isMutating, data, error } = useCreateUser();

  const onSubmit = (data: IFormInput) => {
    trigger(data);
  }; // your form submit function which will invoke after successful validation

triggerの他の使い方

公式ドキュメントにある通り、
新しくリリースされたこのtriggerが便利で、ボタンを押した時に実行することもできます。
onClickonHover等色々なイベントで使えそうです。
また後述にある通り、optionを追加することでエラーハンドリングが出来ます。
成功時はユーザー一覧画面に遷移し、失敗時はエラーポップアップを表示するといった事が出来ます。

<button
    type="button"
    onClick={() => {
      // `createUser` を指定した引数と一緒に実行します
      trigger(data);
    }}
  >
    User1を削除
</button>

ユーザー削除(DELETE API)

カスタムフック

ユーザーを削除するボタンもuseSWRMutationで書いてみました。
これもカスタムフックに内包します。

useDeleteUser.ts
import axios, { AxiosError, AxiosResponse } from 'axios';
import useSWRMutation from 'swr/mutation';
import { Arguments } from 'swr';

// Fetcher implementation.
// The extra argument will be passed via the `arg` property of the 2nd parameter.
// In the example below, `arg` will be `'my_token'`
// Return must return Promise
type Arg = {
  arg: Arguments; // assigned user ID
};
const deleteUser = async (url: string, { arg }: Arg) => {
  // type guard
  const userID: string | undefined = ((arg: Arguments) => {
    if (typeof arg !== 'string') {
      return undefined;
    }
    return arg;
  })(arg);

  // async関数を使う事でawait構文が利用可能になる。
  // await構文の効果は、Promiseインスタンスの.then(val => {})を実行してvalを取り出す
  const res = await axios.delete(`${url}/${userID}`); // concat url and path parameter
  console.log('res ', res.data);

  // Promiseインスタンスでres.dataを返すように変更
  return res.data;
};

// APIから受け取るレスポンスフォーマットを指定する必要があります
export type Response = {
  message: string;
};
const useDeleteUser = () => {
  const { trigger, isMutating, data, error } = useSWRMutation<
    AxiosResponse<Response>,
    AxiosError
  >(`/user`, deleteUser);

  // debug
  console.log('data', data);
  console.log('error', error);

  return { trigger, isMutating, data, error };
};

export default useDeleteUser;

呼び出し元のユーザー削除ボタン

deleteUserButton.ts
import { AxiosError, AxiosResponse } from 'axios';
import useDeleteUser, { Response } from '../hooks/useDeleteUser';

const DeleteUserButton = () => {
  const { trigger, isMutating, data, error } = useDeleteUser();

  // Send a request to the server to delete an user.
  const options = {
    onSuccess(data: AxiosResponse<Response>) {
      console.log('onSuccess: ユーザーの削除に成功', data);
      // router.push('/users'); // ユーザー一覧画面にリダイレクト
    },
    onError(err: AxiosError) {
      console.log('onError: ユーザーの削除に失敗', err);
      throw new Error(err.message);
    },
  };

  return (
    // Delete a User
    <button
      type="button"
      onClick={() => {
        // `deleteUser` を指定した引数と一緒に実行します
        trigger('user_id', options);
      }}
    >
      User1を削除
    </button>
  );
};

export default DeleteUserButton;

triggerのoptionが便利そう

以下のような書き方で、optionを使ってエラーハンドリングが出来ます。
「User1を削除」ボタンを押すとtriggerが走り、以下のようになります。

  • 成功時(APIから200が返却された場合)はonSuccess関数
  • 失敗時(200以外)はonError関数
    これがかなり便利そうな気がします。
const DeleteUserButton = () => {
  const { trigger, isMutating, data, error } = useDeleteUser();

  // Send a request to the server to delete an user.
  const options = {
    onSuccess(data: AxiosResponse<Response>) {
      console.log('onSuccess: ユーザーの削除に成功', data);
      // router.push('/users'); // ユーザー一覧画面にリダイレクト
    },
    onError(err: AxiosError) {
      console.log('onError: ユーザーの削除に失敗', err);
      throw new Error(err.message); // エラー表示(ここでエラーポップアップ表示する実装が良さそうです)
    },
  };

  return (
    // Delete a User
    <button
      type="button"
      onClick={() => {
        // `deleteUser` を指定した引数と一緒に実行します
        trigger('user_id', options);
      }}
    >
      User1を削除
    </button>
  );
};

他にもoptionには色々な機能があり
楽観的UIやその他・詳細に関しては公式ドキュメントを見てほしいです。

Stackbitz

以下にコード保存したので色々いじってみてください。
https://stackblitz.com/edit/next-typescript-useswrmutate-axios?file=pages/index.tsx

まとめ

初めは「もう既にPOSTやPUTはaxiosで直接書いてるし、わざわざaxiosをuseSWRMutationでラップする必要ないわ」と思ってたのですが、useSWRMutationの機能がかなり充実してるので使いこなすと読みやすいコードでより良いUIを実装することができそうです。

SWR2.0リリース前にも、一応SWRでもPOST/PUT/DELETEをする方法としてmutateがあったのですが、あんまり使いやすそうな印象はありませんでした。
ただ、今回のuseSWRMutationのリリースによってGET/PUT/POST/DELETEも全部SWRででぃきらぁあ!となる技術選定が増えてくのかもです。

でぃきらぁあ!

間違いとかあったらガシガシご指摘下さい🙇‍🙇‍

Discussion