Next.js + Typescript + axiosでリリースされたばかりのSWR2.0のuseSWRMutationを試した
はじめに
今まではAPIでGETする時はSWR
でやるものの、PUT/POST/DELETE等はaxios
とかの別ライブラリを使うといった技術選定が多かったように思えます。
今回リリースされたSWR 2.0が新たに提供するuseSWRMutation
によって、いよいよPUT/POST/DELETEも全部SWRで出来ちゃうよって感じになってきた気がします。
SWR 2.0 では、新しいフック useSWRMutation によって、
宣言的な API を使用してリモートでデータを変更することがより簡単になりました。
このフックを使って変異をセットアップし、後でそれを有効にすることができます。
公式ドキュメントにはこのように書かれてるのですが、typescriptのサンプルコードがなかったので自分で作ってみました。
新規ユーザー作成(POST API)
カスタムフック
サンプルで新規ユーザー登録フォームを書いてみました。
useSWRMutation
を内包するカスタムフックを作成
// 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
を呼び出してます。
以下のような流れで処理が走ります。
- formに新規ユーザーの情報(名前・年齢等)を入力しsubmitボタンを押す
-
onSubmit
関数が走り、内部にあるtrigger(data)を経由 - カスタムフック内の
useSWRMutaion
を経由 - axiosでAPIにPOST(なんだかややこしいですね😥)
const { trigger, isMutating, data, error } = useCreateUser();
const onSubmit = (data: IFormInput) => {
trigger(data);
}; // your form submit function which will invoke after successful validation
triggerの他の使い方
公式ドキュメントにある通り、
新しくリリースされたこのtrigger
が便利で、ボタンを押した時に実行することもできます。
onClick
やonHover
等色々なイベントで使えそうです。
また後述にある通り、optionを追加することでエラーハンドリングが出来ます。
成功時はユーザー一覧画面に遷移し、失敗時はエラーポップアップを表示するといった事が出来ます。
<button
type="button"
onClick={() => {
// `createUser` を指定した引数と一緒に実行します
trigger(data);
}}
>
User1を削除
</button>
ユーザー削除(DELETE API)
カスタムフック
ユーザーを削除するボタンもuseSWRMutation
で書いてみました。
これもカスタムフックに内包します。
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;
呼び出し元のユーザー削除ボタン
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
以下にコード保存したので色々いじってみてください。
まとめ
初めは「もう既にPOSTやPUTはaxiosで直接書いてるし、わざわざaxiosをuseSWRMutation
でラップする必要ないわ」と思ってたのですが、useSWRMutation
の機能がかなり充実してるので使いこなすと読みやすいコードでより良いUIを実装することができそうです。
SWR2.0リリース前にも、一応SWRでもPOST/PUT/DELETEをする方法としてmutateがあったのですが、あんまり使いやすそうな印象はありませんでした。
ただ、今回のuseSWRMutation
のリリースによってGET/PUT/POST/DELETEも全部SWRででぃきらぁあ!となる技術選定が増えてくのかもです。
でぃきらぁあ!
間違いとかあったらガシガシご指摘下さい🙇🙇
Discussion