宣言的なコードとは
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 とキャッシュを同期することで、新規ユーザーをリストに追加する
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;
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 と組み合わせて、より宣言的なコードが書ける様になることを期待しています。