SWRを使おうぜという話2022
はじめに
2021年1月に以下のような記事を書きました。
内容はVercel社のオープンソースプロジェクトの一つであるデータフェッチライブラリであるSWRの紹介で、記事内に間違いなどもあったにも関わらずたくさんの反響を頂きました。
2022年半ばとなった今でも「いいね」を頂いております。
しかし、内容は2021年当時のものであり、ライブラリの仕様が少し変更となっておりますので、現在のSWRの仕様に合わせて新しく記事を書くことに致しました。
当記事の内容は「SWRを使おうぜという話」のシナリオに沿っての再掲と致します。
最後までどうぞお付き合いください。
SWRとはなにか
通常、Reactを使用してAPIサーバーからのデータ取得を非同期で行う場合、useEffect
とfetch
やaxios
などのHTTP通信用のクライアントを使用して行われます。
SWRを使わない実装
例として、Next.jsを使用してユーザーのログイン情報を取得するケースを想定します。
Next.jsの場合はSSRをサポートしているため、以下のような書き方ができます。
// サーバーサイドでのデータ取得処理
import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
interface PageProps {
user: { id: string } | null;
}
export async function getServerSideprops({ req }: GerServerSidePropsContext): Promise<GetServerSidePropsResult<PageProps>> {
const user = await getUser(req);
return {
props: {
user
}
};
}
export default function Home({ user }: PageProps) {
return (
<div>
{user ? <p>{user.id}</p> : <a href="/login">Log in</a>}
</div>
);
}
この実装だとサーバーサイドでのレスポンスを待たなくてはならない上にユーザー情報などのパーソナルなデータはキャッシュできないため、クライアント側で取得する操作を採用する場合が多いかと思います。
// クライアントサイドでの取得処理
- import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
+ import { useState, useEffect } from "react";
interface User {
id: string;
}
-export async function getServerSideprops({ req }: GerServerSidePropsContext): Promise<GetServerSidePropsResult<PageProps>> {
- const user = await getUser(req);
- return {
- props: {
- user
- }
- };
-}
export default function Home() {
+ const [user, setUser] = useState<User | null>(null);
+ useEffect(() => {
+ fetch("/api/user", {
+ method: "get",
+ headers: {
+ "Content-Type": "application/json",
+ }
+ })
+ .then((res) => {
+ return res.json() as Promise<User | null>;
+ })
+ .then((data) => {
+ setUser(data);
+ });
+ }, []);
return (
<div>
{user ? <p>{user.id}</p> : <a href="/login">Log in</a>}
</div>
);
}
さらにここに、
- fetchしたuserがnullの場合
- fetchの実行に失敗した場合
- 取得中の処理
が加わると、その状態管理は少し複雑になってきます。
// クライアントサイドでの取得とエラー処理
import { useState, useEffect } from "react";
interface User {
id: string;
}
export default function Home() {
- const [user, setUser] = useState<User | null>(null);
+ const [user, setUser] = useState<User | null | undefined>(undefined);
+ const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/user", {
method: "get",
headers: {
"Content-Type": "application/json",
}
})
.then((res) => {
+ if (res.ok) {
+ return res.json() as Promise<User | null>;
+ }
+ throw new Error(`Some Error`);
})
.then((data) => {
setUser(data);
})
+ .catch((err) => {
+ setError(err.message);
+ setUser(null);
});
}, []);
return (
<div>
- {user ? <p>{user.id}</p> : <a href="/login">Log in</a>}
+ {typeof user === "undefined" ? (
+ <p>loading...</p>
+ ) : user ? (
+ <p>{user.id}</p>
+ ) : (
+ <a href="/login">Login</a>
+ )}
+ {error ? <p>{error}</p> : null}
</div>
);
}
だいぶ複雑になってきました。
しかしこれではページを再度ロードしたときに同じようにデータの取得完了を待たなくてはなりません。
そこでブラウザでのメモリキャッシュを導入したいと思います。
// クライアントサイドでの取得とエラー、キャッシュ処理
import { useState, useEffect } from "react";
interface User {
id: string;
}
+ const cache = new Map<string | User>();
export default function Home() {
- const [user, setUser] = useState<User | null | undefined>(undefined);
+ const [user, setUser] = useState<User | null | undefined>(cache.has("/api/user") ? cache.get("/api/user") : undefined);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/user", {
method: "get",
headers: {
"Content-Type": "application/json",
}
})
.then((res) => {
if (res.ok) {
return res.json() as Promise<User | null>;
}
throw new Error(`Some Error`);
})
.then((data) => {
+ cache.set("/api/user", data);
setUser(data);
})
.catch((err) => {
+ cache.set("/api/user", null);
setError(err.message);
setUser(null);
});
}, []);
return (
<div>
{typeof user === "undefined" ? (
<p>loading...</p>
) : user ? (
<p>{user.id}</p>
) : (
<a href="/login">Login</a>
)}
{error ? <p>{error}</p> : null}
</div>
);
}
ユーザーのログイン情報を判定するだけなのにこれだけの処理を書かなくてはならないのは大変です。眠い時にはうっかりミスをしてしまいそうですね。
SWRに助けてもらおう
SWRは、上記の
- fetchを使用したクライアントサイドのデータ取得
- データ取得状態の管理
- エラー処理
- データキャッシュ
を少ない記述で実現することが出来ます。
まずは上記の例を書き換えてみましょう。
// SWRでの実装
- import { useState, useEffect } from "react";
+ import useSWR from "swr";
interface User {
id: string;
}
- const cache = new Map<string | User>();
async function fetcher(key: string, init?: RequestInit) {
return fetch(key, init).then((res) => res.json() as Promise<User | null>);
}
export default function Home() {
- const [user, setUser] = useState<User | null | undefined>(undefined);
- const [user, setUser] = useState<User | null | undefined>(cache.has("/api/user") ? cache.get("/api/user") : undefined);
- const [error, setError] = useState<string | null>(null);
+ const { data: user, error } = useSWR("/api/user", fetcher);
- useEffect(() => {
- fetch("/api/user", {
- method: "get",
- headers: {
- "Content-Type": "application/json",
- }
- })
- .then((res) => {
- if (res.ok) {
- return res.json() as Promise<User | null>;
- }
- throw new Error(`Some Error`);
- })
- .then((data) => {
- cache.set("/api/user", data);
- setUser(data);
- })
- .catch((err) => {
- cache.set("/api/user", null);
- setError(err.message);
- setUser(null);
- });
- }, []);
return (
<div>
{typeof user === "undefined" ? (
<p>loading...</p>
) : user ? (
<p>{user.id}</p>
) : (
<a href="/login">Login</a>
)}
{error ? <p>{error}</p> : null}
</div>
);
}
ものすごく簡単になりましたね!!
SWRについて
ここからはSWRというライブラリについて紹介していきます。
SWRでは何を行っているのか
基本となるuseSWR関数は
-
key
: リクエストするユニークな文字列(通常URLを指定する) -
fetcher
: (任意)第一引数に渡したURLを引数に取るfetch関数 -
options
: (任意)SWRのオプション
の3つの引数を取ります。
この時第一引数に渡したkeyは、ブラウザでのデータキャッシュキーも兼ねています。
そしてこの関数は
-
data
:fetcher
によって取得したデータ -
error
:fetcher
によってthrowされたエラー -
isValidating
: リクエストまたは再検証の読み込みがあるか否かのBool値 -
mutate
: キャッシュされたデータを更新する関数
を返却します。
このとき、data
の状態は - 取得が完了していない場合は
undefined
- 取得が完了した場合はその内容
となり、data
がundefined
か否かで読み込み中かどうかをUIに伝えることが出来ます。
SWRでのオプション群
const { data, error, isValidating, mutate } = useSWR("/api/user", fetcher, {
// options
});
SWRにはたくさんのオプションがあり、このオプションによりデータ操作をより細かく扱うことが出来ます。
代表的なものをいくつかご紹介します。
全てのオプションは公式ドキュメントをご参照ください。
revalidateIfStale
: 古いデータの再検証
デフォルト: true
データが取得済みであるときに、ページ遷移や他のコンポーネントで同フック同キーのuseSWR
使用した場合にもデータの再検証を行い、新しいデータに更新します。
revalidateOnFocus
: ウィンドウのフォーカス時の再検証
デフォルト: true
ブラウザから作業を離れて、再度ブラウザにフォーカスをしたときにデータの再取得を試みます。
revalidateOnReconnect
: ブラウザのネットワーク接続回復時の再検証
デフォルト: true
ブラウザのネットワーク接続が切れた後に、再度接続が回復したと同時にデータを再検証します。
チャットなどのリアルタイムアプリケーションなどのデータ取得時に効果を発揮しそうですね。
refreshInterval
: データの再検証時間
デフォルト: 0
(無効)
数値を設定すると、<設定値>ミリ秒単位でポーリング取得(断続的にデータ取得を実行)を試みます。
こちらもリアルタイムアプリケーションなどで効果を発揮します。
shouldRetryOnError
: エラー発生時の再実行
デフォルト: true
データの取得に失敗した場合に、再検証を試みます。
errorRetryInterval
, errorRetryCount
: エラー再実行設定
デフォルト: 5000
, undefined
上記のshouldRetryOnError
が有効の場合に、その
- その再取得実行の間隔
- 再取得の実行回数
をそれぞれ指定できます。
fallbackData
: 初期データ
データ取得が完了する前の初期データを注入できます。
グローバルオプション
全てのSWRフックで適用したいオプションがあるならば、Context
で設定を渡すことが出来ます。
エラーハンドリングやfetcher
関数などは毎回作成が面倒な場合にグローバルに設定することがままあります。
import useSWR, { SWRConfig } from "swr";
async function fetcher(key: string, init?: RequestInit) {
return fetch(key, init).then((res) => res.json());
}
function User() {
const { data } = useSWR("/api/user");
/// ...
}
function App() {
return (
<SWRConfig
value={{
revalidateIfStale: false,
fetcher
}}
>
<User />
</SWRConfig>
);
}
データの更新処理
通常はデータの取得に使用するSWRですが、mutate関数を使用して更新処理の実装も可能です。
export default function Home() {
const { data, mutate } = useSWR("/api/user");
const [name, setName] = useState("");
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await mutate(async (user) => {
const res = await fetch("/api/user", {
method: "post",
body: JSON.stringify({ id: user.id, name }),
headers: { "Content-Type": "application/json" }
});
const data = await res.json();
return data;
});
}, [mutate, name]);
return (
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={(e) => setName(e.currentTarget.value)} />
<button type="submit">Submit</button>
</form>
);
}
mutate関数では、
- 取得済みのデータを引数に取り
- 新しいデータを返却する
非同期関数を引数に渡すことで、データの更新を行います。
この時関数内で行うHTTPリクエストで、POSTやPATCHなどの他のメソッドを使用したデータ更新処理を行うことが出来ます。
そのほかの仲間たち
通常のuseSWR
の他に、
- 大量のデータをページングするための
useSWRInfinite
- データを一度だけ取得する
useSWRImmutable
- SWRの設定を取得する
useSWRConfig
などのHooksが用意されており、クライアントサイドのデータ取得に関わる殆どのUI処理パターンを網羅できるかと思われます。
さいごに
Reactでのデータ取得→状態管理の流れは、実装を間違えると複雑かつ依存だらけとなってしまいます。
また、アプリが巨大化してくるほど、そのロジックの一部を変更するだけでも一苦労です。
データを取得したり更新したりするバックエンドAPIのエンドポイントと返却するデータ型さえ決まっていれば、どのプロジェクトのどのコンポーネントでも使用できるはうれしいですね。
現状のアプリケーションの一部だけでも使用できるので、是非試しに使ってみてください。
最後までお読みくださり有難うございました。
Discussion