環境
React:v18.3.1
SWR:v2.2.5
isLoadingとisValidatingの動作確認では次のバージョンで実行した。
React:v19.1.0
SWR:v2.3.4
インストール
下記コマンドを実行してインストールする。
% npm install swr
テスト用簡易サーバを用意
HTTPリクエストを受け取ってから2秒後にレスポンスを返す簡易サーバを用意する。
Expressのインストール
% mkdir フォルダ名 && cd フォルダ名
% npm init
% npm install express
// インストールされたExpressのバージョンは4.19.2だった。
% vim app.js
app.jsの内容
const express = require('express')
const app = express()
const port = 3001
function sendResponse(res) {
res.send('ApiServer : ' + Date());
}
const allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, access_token'
)
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.send(200)
} else {
next()
}
}
app.use(allowCrossDomain)
app.get('/', (req, res) => {
setTimeout(
sendResponse,
2000,
res
);
// console.log('req = ', req);
// console.log('res = ', res);
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
上記app.jsをコピペしたら、下記コマンドを実行してサーバを起動する。
% nvm use node
% node app.js
サーバ起動後、Webブラウザで http://localhost:3001 へアクセスし、
「ApiServer : (日時)」と2秒後に表示されれば良い。
App.tsxの内容
import React, {
createContext, ReactNode, Suspense, useEffect, useLayoutEffect,
} from 'react';
import UseSwr from './UseSwr.tsx';
import Fetch from './Fetch.tsx';
return (
<>
<Fetch />
<hr />
<Suspense fallback={<div>Now Loading...</div>}>
<UseSwr />
</Suspense>
</>
);
}
export default App;
Fetch.tsxの内容
import { ReactNode, useState } from 'react';
function Fetch(): ReactNode {
const [showFetchData, setShowFetchData] = useState<Promise<string>|string>('初期値');
const [startFetchData, setStartFetchData] = useState<string>('初期値');
const [endFetchData, setEndFetchData] = useState<string>('初期値');
const [countFetchData, setCountFetchData] = useState<number>(0);
async function fetchData(): Promise<void> {
setStartFetchData(Date());
const response: Response = await fetch('http://localhost:3001');
const promiseText: string = await response.text();
setShowFetchData(promiseText);
setEndFetchData(Date());
setCountFetchData(countFetchData + 1);
}
return (
<>
<div>
{`countFetchData = ${countFetchData}`}
<br />
{`startFetchData = ${startFetchData}`}
<br />
{`showFetchData = ${showFetchData}`}
<br />
{`endFetchData = ${endFetchData}`}
</div>
<div>
<button
type="button"
onClick={fetchData}
>
fetchData
</button>
</div>
</>
);
}
export default Fetch;
UseSwr.tsxの内容
import { ReactNode, useState } from 'react';
import useSWR, { useSWRConfig } from 'swr';
function UseSwr(): ReactNode {
// swrData関数用。
const [startSwrData, setStartSwrData] = useState<string>('初期値');
const [endSwrData, setEndSwrData] = useState<string>('初期値');
const [countSwrData, setCountSwrData] = useState<number>(0);
const [aboutCache, setAboutCache] = useState<ReactNode>(null);
// キャッシュボタンのイベントハンドラで使用。
const config = useSWRConfig();
// useSWRフックで使用されるfetcher。
async function fetcher(url: string): Promise<string> {
setStartSwrData(Date());
const promiseFetch: Response = await fetch(url);
setEndSwrData(Date());
setCountSwrData(countSwrData + 1);
return promiseFetch.text();
}
// useSWRフックが成功した後に実行される関数。
function swrOnSuccess(): void {
console.log('swrOnSuccess');
setCountSwrData(countSwrData + 1);
}
const { data, mutate } = useSWR<string>(
'http://localhost:3001',
fetcher,
{
suspense: true, // Suspenseコンポーネント用。20240521_非推奨となっている。
onSuccess: swrOnSuccess, // 成功した後に実行される関数。
revalidateIfStale: true, // 古いデータがあっても更新する。
revalidateOnFocus: false, // フォーカスされても更新しない。
revalidateOnReconnect: false, // ネットワーク接続復帰時に更新しない。
},
);
const getAboutCache = () => {
let component: ReactNode = '';
config.cache.entries().forEach(([key, value]) => {
component += `<div><div>key = ${key}</div><div>value.data = ${value.data}</div></div><br/>`;
});
component += `<div>config.cache.size = ${config.cache.size}</div>`;
setAboutCache(component);
};
const handleRefresh = () => {
// バウンドミューテートを利用。
// Suspenseのfallbackは表示されない。
mutate();
};
return (
<>
<div>
{`countSwrData = ${countSwrData}`}
<br />
{`startSwrData = ${startSwrData}`}
<br />
{`showSwrData = ${data}`}
<br />
{`endSwrData = ${endSwrData}`}
</div>
<div>
<button
type="button"
onClick={getAboutCache}
>
キャッシュ
</button>
</div>
{aboutCache
&& (
<>
<br />
<div dangerouslySetInnerHTML={{ __html: aboutCache }} />
</>
)}
<br />
<button
type="button"
onClick={handleRefresh}
>
更新
</button>
</>
);
}
export default UseSwr;
結果

useSWRを利用したコンポーネントの作成例
import { ReactNode } from 'react';
import useSWR from 'swr';
async function fetcher(tempUrl: string): Promise<string|null> {
const response = await fetch(tempUrl);
return response.text();
}
/**
* useSWRを利用したコンポーネントを作成する場合の例。
* @constructor
*/
function UseSwrComponent(): ReactNode {
const url: string = 'http://localhost:3001';
const {
data, error, isLoading, isValidating, mutate,
} = useSWR(
url,
fetcher,
);
if (error) return <div>error</div>;
if (isLoading) return <div>loading</div>;
if (isValidating) return <div>validating</div>;
function handleBtnMutate() {
mutate();
}
return (
<div>
<div>
{data}
</div>
<div>
<button
type="button"
onClick={handleBtnMutate}
>
mutate
</button>
</div>
</div>
);
}
export default UseSwrComponent;
コンポーネントの一部にuseSWRを利用したデータを表示する例(State不使用)
import { ReactNode } from 'react';
import useSWR from 'swr';
async function fetcher(tempUrl: string): Promise<string|null> {
const response = await fetch(tempUrl);
return response.text();
}
/**
* コンポーネントの一部分にuseSWRを利用したデータを表示する場合の例。
* Stateを使わないやり方。
* @constructor
*/
function ContainUseSwrData(): ReactNode {
const url: string = 'http://localhost:3001';
let contents: string = 'loading or validating';
const {
data, error, isLoading, isValidating, mutate,
} = useSWR(
url,
fetcher,
);
// letで定義された変数でなく、useStateで作成されたset関数を利用するとTooManyReRenderのエラーとなる。
if (error) contents = 'error';
if (!isLoading && !isValidating) contents = String(data);
function handleBtnMutate() {
mutate();
}
return (
<div>
<div>
{contents}
</div>
<div>
<button
type="button"
onClick={handleBtnMutate}
>
mutate
</button>
</div>
</div>
);
}
export default ContainUseSwrData;
コンポーネントの一部にuseSWRを利用したデータを表示する例(State使用)
import { ReactNode, useEffect, useState } from 'react';
import useSWR from 'swr';
async function fetcher(tempUrl: string): Promise<string> {
const response = await fetch(tempUrl);
return response.text();
}
/**
* コンポーネントの一部分にuseSWRを利用したデータを表示する場合の例。
* Stateを利用するやり方。
* @constructor
*/
function ContainUseSwrData2(): ReactNode {
const url: string = 'http://localhost:3001';
const [contents, setContents] = useState<string>('');
let tempContents = 'loading or validating';
const {
data, error, isLoading, isValidating, mutate,
} = useSWR(
url,
fetcher,
);
if (error) tempContents = 'error';
if (!isLoading && !isValidating) tempContents = String(data);
useEffect(() => {
setContents(tempContents);
}, [tempContents]);
function handleBtnMutate() {
mutate();
}
return (
<div>
<div>
{contents}
</div>
<div>
<button
type="button"
onClick={handleBtnMutate}
>
mutate
</button>
</div>
</div>
);
}
export default ContainUseSwrData2;
isLoadingとisValidatingの動作確認
概要
上記のテスト用簡易サーバを起動し、下記Default.tsxコンポーネントをWebブラウザで表示し、mutateボタンを実行する。
Default.tsxの内容
import { type ReactNode, useLayoutEffect } from 'react'
import useSWR from 'swr'
function Default(): ReactNode {
const {data, isLoading, isValidating, error, mutate} = useSWR<string>(
'http://localhost:3001',
async (url: string): Promise<string> => {
const response = await fetch(url);
return response.text();
}
);
console.log('=======================================================');
console.log(`data = ${data}`);
console.log(`isLoading = ${isLoading}`);
console.log(`isValidating = ${isValidating}`);
console.log(`error = ${error}`);
console.log('=======================================================');
useLayoutEffect(() => {
console.log('初期描画');
}, []);
const clickBtnMutate = () => {
console.log('mutateボタンがクリックされました');
mutate();
}
return (
<>
Default.tsx
<br />
<button type="button" onClick={clickBtnMutate}>
mutate
</button>
</>
);
}
export default Default;
結果(コンソールの表示内容)
=======================================================
data = undefined
isLoading = true
isValidating = true
error = undefined
=======================================================
初期描画
=======================================================
data = ApiServer : Fri Jul 18 2025 15:55:38 GMT+0900 (日本標準時)
isLoading = false
isValidating = false
error = undefined
=======================================================
mutateボタンがクリックされました
=======================================================
data = ApiServer : Fri Jul 18 2025 15:55:38 GMT+0900 (日本標準時)
isLoading = false
isValidating = true
error = undefined
=======================================================
=======================================================
data = ApiServer : Fri Jul 18 2025 15:55:44 GMT+0900 (日本標準時)
isLoading = false
isValidating = false
error = undefined
=======================================================
mutateにdataパラメータを渡した時の動作確認
概要
上記のテスト用簡易サーバを起動し、下記Default.tsxコンポーネントをWebブラウザで表示し、mutateボタンを実行する。
Default.tsxの内容
import { type ReactNode, useLayoutEffect } from 'react'
import useSWR from 'swr'
function Default(): ReactNode {
const {data, isLoading, isValidating, error, mutate} = useSWR<string>(
'http://localhost:3001',
async (url: string): Promise<string> => {
const response = await fetch(url);
return response.text();
}
);
console.log('=======================================================');
console.log(`data = ${data}`);
console.log(`isLoading = ${isLoading}`);
console.log(`isValidating = ${isValidating}`);
console.log(`error = ${error}`);
console.log('=======================================================');
useLayoutEffect(() => {
console.log('初期描画');
}, []);
const clickBtnMutateWithOutData = () => {
console.log('mutate(without data)ボタンがクリックされました');
mutate();
}
const clickBtnMutateWithData = () => {
console.log('mutate(with data)ボタンがクリックされました');
mutate('abc');
}
return (
<>
Default.tsx
<br />
<button type="button" onClick={clickBtnMutateWithOutData}>
mutate(without data)
</button>
<br />
<button type="button" onClick={clickBtnMutateWithData}>
mutate(with data)
</button>
</>
);
}
export default Default;
結果(コンソールの表示内容)
=======================================================
data = undefined
isLoading = true
isValidating = true
error = undefined
=======================================================
初期描画
=======================================================
data = ApiServer : Fri Jul 18 2025 17:02:07 GMT+0900 (日本標準時)
isLoading = false
isValidating = false
error = undefined
=======================================================
mutate(without data)ボタンがクリックされました
=======================================================
data = ApiServer : Fri Jul 18 2025 17:02:07 GMT+0900 (日本標準時)
isLoading = false
isValidating = true
error = undefined
=======================================================
=======================================================
data = ApiServer : Fri Jul 18 2025 17:02:12 GMT+0900 (日本標準時)
isLoading = false
isValidating = false
error = undefined
=======================================================
mutate(with data)ボタンがクリックされました
=======================================================
data = abc
isLoading = false
isValidating = true
error = undefined
=======================================================
=======================================================
data = ApiServer : Fri Jul 18 2025 17:02:17 GMT+0900 (日本標準時)
isLoading = false
isValidating = false
error = undefined
=======================================================
mutateの取得結果が前回と同一の時に描画するかの確認
概要
Expressから返される内容を同一のものに変更するために次の変更を行う。
テスト用簡易サーバのapp.jsの6行目を次のように変更する。
変更前:res.send(‘ApiServer : ‘ + Date());
変更後:res.send(‘ApiServer’);
描画されるかどうかはuseLayoutEffectフックを利用して確認する。
変更済みのテスト用簡易サーバを起動し、下記Draw.tsxコンポーネントをWebブラウザで表示し、mutateボタンを実行する。
Draw.tsxの内容
import { type ReactNode, useLayoutEffect } from 'react'
import useSWR from 'swr'
function Draw(): ReactNode {
const {data, isLoading, isValidating, error, mutate} = useSWR<string>(
'http://localhost:3001',
async (url: string): Promise<string> => {
const response = await fetch(url);
return response.text();
}
);
console.log('=======================================================');
console.log(`data = ${data}`);
console.log(`isLoading = ${isLoading}`);
console.log(`isValidating = ${isValidating}`);
console.log(`error = ${error}`);
console.log('=======================================================');
useLayoutEffect(() => {
console.log('初期描画');
}, []);
useLayoutEffect(() => {
console.log('描画');
});
const clickBtnMutate = () => {
mutate();
}
return (
<>
Draw.tsx
<br />
<button type="button" onClick={clickBtnMutate}>
mutate
</button>
<br />
data = {data}
</>
);
}
export default Draw;
結果(コンソールの表示内容)
=======================================================
data = undefined
isLoading = true
isValidating = true
error = undefined
=======================================================
初期描画
描画
=======================================================
data = ApiServer
isLoading = false
isValidating = false
error = undefined
=======================================================
描画
=======================================================
data = ApiServer
isLoading = false
isValidating = true
error = undefined
=======================================================
描画
=======================================================
data = ApiServer
isLoading = false
isValidating = false
error = undefined
=======================================================
描画
キャッシュのdataを削除の動作確認
概要
キャッシュの情報を表示し、削除する動作確認。
キャッシュの表示方法は本Webページ上部の方法と少し違う。
キャッシュのdataを削除するだけで、キーは消えない。
テスト用簡易サーバを起動し、下記Cache.tsxコンポーネントをWebブラウザで表示し、mutateボタンを押下し、キャッシュボタンを削除ボタンを押下する。
Cache.tsxの内容
import type { ReactNode } from 'react'
import useSWR, { mutate, useSWRConfig } from 'swr'
function Cache(): ReactNode {
const url: string = 'http://localhost:3001';
const {cache} = useSWRConfig();
const {data} = useSWR<string>(
url,
async (url: string): Promise<string> => {
const response = await fetch(url);
return response.text();
}
);
console.log('=================================');
console.log('キャッシュ情報');
for (const key of cache.keys()) {
console.log(`cache key = ${key}`);
console.log(`cache data = ${cache.get(key)?.data}`);
}
console.log('=================================');
const deleteCache = () => {
console.log('キャッシュを削除ボタンが押下されました');
mutate(url, undefined, {revalidate: false});
}
const refresh = () => {
console.log('mutateボタンが押下されました');
mutate(url);
}
return (
<>
<button type={'button'} onClick={refresh}>
mutate
</button>
<br />
<button type={'button'} onClick={deleteCache}>
キャッシュを削除
</button>
<br />
data = {data}
</>
);
}
export default Cache;
結果(コンソールの表示内容)
=================================
キャッシュ情報
=================================
=================================
キャッシュ情報
cache key = http://localhost:3001
cache data = ApiServer : Fri Jul 18 2025 23:10:39 GMT+0900 (日本標準時)
=================================
mutateボタンが押下されました
=================================
キャッシュ情報
cache key = http://localhost:3001
cache data = ApiServer : Fri Jul 18 2025 23:10:44 GMT+0900 (日本標準時)
=================================
キャッシュを削除ボタンが押下されました
=================================
キャッシュ情報
cache key = http://localhost:3001
cache data = undefined
=================================
keepPreviousDataの動作確認
概要
keepPreviousDataオプションが有効な状態と、無効な状態を比較する。
useSWRのキーを変更する場合に役立つこともあるはず。
テスト用簡易サーバを起動し、下記KeepPreviousData.tsxコンポーネントをWebブラウザで表示し、キーを変更ボタンを押下する。
KeepPreviousData.tsxの内容
import { type ReactNode, useState } from 'react'
import useSWR from 'swr'
function KeepPreviousData(): ReactNode {
const url: string = 'http://localhost:3001';
const [swrKey, setSwrKey] = useState<number>(0);
const {data} = useSWR<string>(
swrKey.toString(),
async (): Promise<string> => {
const response = await fetch(url);
return response.text();
},
{
keepPreviousData: true,
}
);
console.log(`data = ${data}`);
const clickBtn = () => {
console.log('キーを変更ボタンが押下されました');
setSwrKey(swrKey + 1);
}
return (
<>
<button type={'button'} onClick={clickBtn}>
キーを変更
</button>
<br />
data = {data}
</>
);
}
export default KeepPreviousData;
結果(コンソールの表示内容)
keepPreviousDataが無効な場合
data = undefined
data = ApiServer : Fri Jul 18 2025 23:55:49 GMT+0900 (日本標準時)
キーを変更ボタンが押下されました
data = undefined
data = ApiServer : Fri Jul 18 2025 23:55:54 GMT+0900 (日本標準時)
keepPreviousDataが有効な場合
data = undefined
data = ApiServer : Fri Jul 18 2025 23:57:37 GMT+0900 (日本標準時)
キーを変更ボタンが押下されました
data = ApiServer : Fri Jul 18 2025 23:57:37 GMT+0900 (日本標準時)
data = ApiServer : Fri Jul 18 2025 23:57:42 GMT+0900 (日本標準時)
populateCacheの動作確認
目的
既に取得したデータがある状態で、populateCacheオプションを設定してmutateする。
既存のデータとAPIから取得したデータが同じであれば何もしない。
既存のデータとAPIから取得したデータが違っていれば、
何らかのメッセージをユーザに表示し、
Yesが選択されたら更新し、Noが選択されたら更新しない、という処理を実現したい。
結果
実際にはモーダルやボタンをコンポーネント化するだろうけど、とりあえずできた。
Expressを利用した簡易APIサーバ
POSTでJSONを受け取る。
{testName: ‘useSWR’}というJSONを受け取ったら、時間を返す。
{testName: ‘mutate1’}というJSONを受け取ったら、mutate1の文字列を返す。
{testName: ‘mutate2’}というJSONを受け取ったら、mutate2の文字列を返す。
const express = require('express')
const app = express()
const port = 3001
const allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, access_token'
)
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.sendStatus(200)
} else {
next()
}
}
app.use(allowCrossDomain)
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.post('/', (req, res) => {
const testName = req.body.testName
console.log(`testName = ${testName}`)
if (testName === 'useSWR') {
res.send('ApiServer: ' + Date())
} else if (testName === 'mutate1') {
res.send('mutate1')
} else {
res.send('mutate2')
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
コード
import { type ReactNode, useEffect, useState } from 'react'
import useSWR, { useSWRConfig } from 'swr'
function PopulateCache(): ReactNode {
// APIのURL。
const url: string = 'http://localhost:3001';
// 初期描画時等に実行されるuseSWR。
// サーバに{testName: 'useSWR'}というJSONが送信される。
const {data} = useSWR<string>(
url,
async (url: string): Promise<string> => {
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({testName: 'useSWR'}),
});
return response.text();
},
);
// グローバルmutate取得。
const {mutate} = useSWRConfig();
// 更新可能なデータを格納するstateで、モーダル表示・非表示の視覚状態を表す。
const [newData, setNewData] = useState<string|null>(null);
// mutate(with PopulateCache)ボタンのイベントハンドラ。
function mutateWithPopulateCache(num: number) {
// サーバに{testName: 'mutate+数字'}というJSONが送信される。
mutate(
url,
// もし第1引数が存在すれば、第1引数に現在のdataの値が入る。
async (): Promise<string> => {
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({testName: `mutate${num}`}),
});
return response.text();
},
{
// mutateの第2引数に指定した関数実行後、キャッシュの再検証のためにAPIと2重で通信するのを防ぐ。
revalidate: false,
// mutateの第2引数に指定した関数実行後、その通信結果とキャッシュを比べて更新する処理。
// 第1引数にmutateの第2引数に指定した関数の戻り値が入り、第2引数にキャッシュデータが入る。
populateCache: function (newData: string, oldData: string|undefined): string {
console.log('===== populateCache =====');
console.log(`newData = ${newData}`);
console.log(`oldData = ${oldData}`);
// 更新可能なデータがあった場合。
if (newData !== oldData) {
setNewData(newData);
}
return oldData ?? '';
},
},
)
}
useEffect(() => {
console.log(`data = ${data}`);
}, [data]);
useEffect(() => {
if (newData !== null) {
(document.getElementById('dialog') as HTMLDialogElement).showModal();
}
}, [newData]);
// モーダルの「はい」ボタンのイベントハンドラ。
function handleBtnYes(): void {
mutate(url, newData, {revalidate: false});
(document.getElementById('dialog') as HTMLDialogElement).close();
setNewData(null);
}
// モーダルの「いいえ」ボタンのイベントハンドラ。
function handleBtnNo(): void {
(document.getElementById('dialog') as HTMLDialogElement).close();
setNewData(null);
}
return (
<>
data = {data}
<hr/>
<button type={'button'} onClick={() => {
mutateWithPopulateCache(1)
}}>
mutate1(with PopulateCache)
</button>
<br/>
<br/>
<button type={'button'} onClick={() => {
mutateWithPopulateCache(2)
}}>
mutate2(with PopulateCache)
</button>
<dialog id="dialog">
<form method="dialog">
更新可能なデータがあります。<br/>
更新してよろしいですか?<br/>
<button
type="button"
id="btnYes"
onClick={handleBtnYes}
>
はい
</button>
<br/>
<button
type="button"
id="btnNo"
onClick={handleBtnNo}
>
いいえ
</button>
</form>
</dialog>
</>
);
}
export default PopulateCache;