次世代のフロントエンド開発では、データの取得とその最適化が重要な要素です。この記事では、Next.js 13のappディレクトリを使用して、SWRというデータフェッチングライブラリを活用してデータの取得を最適化する方法を解説します。さらに、React Queryとの違いも考察します。
キャッシュベースのAPI通信ってなんだかすごそう!
キャッシュベースにすることでAPI通信を最小限にし余計なエラーなどを起こさないようにできるんだ。
1. Next.js 13のappディレクトリとは
Next.js 13では、新しくappディレクトリが導入されました。このディレクトリは、従来のpagesディレクトリとは異なり、ルーティングを自動的に生成せず、コンポーネント、ヘルパー、フックなどの共有ロジックを配置するのに適しています。
プロジェクトのフォルダ構成は以下のようになります:
my-next-app/
├── app/
│ ├── api/
│ ├── page.tsx
│ ├── global.css
│
├── hooks/
├── public/
└── components/
2. SWRとは
SWRは、Reactのデータフェッチングライブラリであり、クライアントサイドでリモートデータを効果的にフェッチし、キャッシュするのに役立ちます。SWRは “Stale While Revalidate” の略で、キャッシュされたデータを表示しながらバックグラウンドで新しいデータをフェッチし、更新する戦略をとります。
SWRの基本的な使い方:
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
3. React Queryとの違い
React Queryもデータフェッチングのための人気ライブラリですが、SWRとはいくつかの点で異なります。
- APIの設計思想: SWRはRESTful APIに優れている一方で、React QueryはRESTful APIとGraphQLの両方に適しています。
- デフォルトのリフェッチ戦略: SWRはデフォルトで”Stale While Revalidate
“戦略を採用しているのに対し、React Queryはより多くの設定オプションを提供します。
4. SWRを使ったAPI連携の例
Next.jsのappディレクトリ内でSWRを使用してAPIと連携する例を見てみましょう。
hooks/useTodos.ts
import useSWR from 'swr';
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
export const useTodos = () => {
const { data, error } = useSWR('/api/todos', fetcher);
return {
todos: data,
isLoading: !error && !data,
isError: error
};
};
app/components/TodoList.tsx
import { useTodos } from '../hooks/useTodos';
const TodoList = () => {
const { todos, isLoading, isError } = useTodos();
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading todos</div>;
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
};
export default TodoList;
5. エラーハンドリングとローデイング管理
データフェッチングはウェブアプリケーションの中核的な部分ですが、ネットワークの不安定さやサーバーの問題など、様々なエラーが生じる可能性があります。また、データを取得するまでの間、ユーザーにはローディング状態を表示する必要があります。SWRはこれらの課題を解決するための素晴らしいライブラリです。
SWRにおけるエラーハンドリング
SWRは、データフェッチングの結果としてのエラーを簡単にハンドルすることができます。useSWR
フックは、データとともにエラーオブジェクトも返します。これを使用して、エラーが発生した場合のUIを条件付きで表示することができます。
const { data, error } = useSWR('/api/data', fetcher);
if (error) return <div>Failed to load data</div>;
if (!data) return <div>Loading...</div>;
return <div>{data}</div>;
ローディング状態の管理
上記の例では、data
が未定義の場合、SWRはローディング状態であると解釈します。これを利用して、データがロードされるまでの間にローディングスピナーや他のプレースホルダーを表示することができます。
ローディングコンポーネントの例
import React from "react";
import CircularProgress from "@mui/material/CircularProgress";
import { Box, Typography, keyframes } from "@mui/material";
const fadeInOut = keyframes`
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
export const APISpinner = () => {
return (
<div
style={{
position: "fixed",
width: "100vw",
height: "100vh",
top: "0",
left: "0",
display: "flex",
justifyContent: "center",
alignItems: "center",
// transform: "translate(-50%, -50%)",
backgroundColor: "rgba(0, 0, 0,.6)",
zIndex: 99,
}}
>
<Box>
<CircularProgress
sx={{
color: "#ffff00",
width: "80px!important",
height: "80px!important",
}}
/>
<Typography
component="div"
sx={{
fontSize: "1.2rem",
fontWeight: "bold",
animation: `${fadeInOut} 1.5s infinite`,
color: "gray.0",
fontWeight: "bold",
}}
>
Loading...
</Typography>
</Box>
</div>
);
};
グローバルエラーハンドリング
また、SWRでは、アプリケーション全体で共通のエラーハンドリングロジックを定義することも可能です。これにより、一貫したエラーハンドリングとユーザーエクスペリエンスを簡単に実現できます。
6. 実用例
このセクションでは、Next.jsのプロジェクトでSWRを使用して、リアルタイムの天気情報を取得する例を見てみましょう。
まず、app/hooks
ディレクトリ内にuseWeather.ts
というフックを作成します。
hooks/useWeather.ts
import useSWR from 'swr';
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
export const useWeather = (city) => {
const { data, error } = useSWR(`https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=${city}`, fetcher);
return {
weather: data,
isLoading: !error && !data,
isError: error
};
};
components/WeatherInfo.tsx
import { useWeather } from '../hooks/useWeather';
const WeatherInfo = ({ city }) => {
const { weather, isLoading, isError } = useWeather(city);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading weather</div>;
return (
<div>
<h2>{weather.location.name}</h2>
<p>{weather.current.temp_c}°C</p>
</div>
);
};
export default WeatherInfo;
7. mutateの利用
SWR ライブラリの mutate
関数は、キャッシュされたデータを効率的に更新するための強力なツールです。これにより、サーバーに変更を加えた後、UI をリアルタイムで更新できます。mutate
は、ローカルのキャッシュを直接更新するか、サーバーからデータを再取得してキャッシュを更新するか、どちらでも対応できます。
mutateの基本的な使い方
1. 再検証(Revalidation)
データを変更した後、キャッシュをサーバーのデータと同期させたい場合、mutate
関数を使用してキャッシュを再検証します。
import { mutate } from 'swr';
const updateTodo = async (id, text) => {
await fetch(`/api/todos/${id}`, { method: 'PUT', body: JSON.stringify({ text }) });
// キャッシュを再検証
mutate('/api/todos');
};
ここでは、mutate
関数を使って、ToDo アイテムを更新した後、/api/todos
エンドポイントのキャッシュを再検証しています。
2. オプティミスティックUI更新(Optimistic UI Updates)
ユーザーがアクションを実行した際に即座に UI を更新し、後からサーバーと同期させることをオプティミスティックUI更新といいます。
import { mutate } from 'swr';
const updateTodo = async (id, newText) => {
// キャッシュを即時更新
mutate('/api/todos', todos.map(todo => (todo.id === id ? { ...todo, text: newText } : todo)), false);
// データをサーバーに更新
await fetch(`/api/todos/${id}`, { method: 'PUT', body: JSON.stringify({ text: newText }) });
// キャッシュを再検証
mutate('/api/todos');
};
上記のコードでは、最初に mutate
関数を使ってキャッシュを即座に更新し、ユーザーに反映されるようにしています。その後、データをサーバーに送信し、再び mutate
関数を使ってキャッシュを最新の状態に更新しています。
注意点
mutate
の第三引数にfalse
を設定すると、キャッシュを直接更新するだけでなく、再検証をスキップします。- オプティミスティックUI更新は、ユーザーにとってレスポンスが
早く感じる反面、サーバーとの同期が取れていない状態が一時的に発生することがあるため、慎重に使用する必要があります。
mutate 関数は SWR のキャッシュ管理を非常に柔軟に行うことができるため、API のデータフェッチと同期において優れたユーザーエクスペリエンスを提供します。これにより、高速なレスポンスとデータの一貫性を両立させることができます。
フロント側の実装
まず、TodoItem
コンポーネントを考えましょう。このコンポーネントは、各ToDoアイテムのテキストを表示し、そのテキストを編集する機能を持っています。
import { useState } from 'react';
import { mutate } from 'swr';
const TodoItem = ({ todo }) => {
const [isEditing, setIsEditing] = useState(false);
const [newText, setNewText] = useState(todo.text);
const handleUpdate = async () => {
// オプティミスティック UI 更新
const updatedTodos = todos.map(item =>
item.id === todo.id ? { ...item, text: newText } : item
);
mutate('/api/todos', updatedTodos, false);
// サーバーに更新を送信
await fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newText }),
});
// キャッシュを再検証して最新のデータを取得
mutate('/api/todos');
setIsEditing(false);
};
return (
<div>
{isEditing ? (
<>
<input
type="text"
value={newText}
onChange={(e) => setNewText(e.target.value)}
/>
<button onClick={handleUpdate}>Save</button>
</>
) : (
<div onClick={() => setIsEditing(true)}>
{todo.text}
</div>
)}
</div>
);
};
このコンポーネントでは、ToDo アイテムをクリックすると編集モードになり、テキストを変更して保存ボタンをクリックすると、その変更がサーバーに送信されます。
重要な部分は handleUpdate
関数です。この関数内で以下の手順を踏んでいます。
mutate
を使用してオプティミスティック UI 更新を行い、ユーザーに即座に変更を表示します。fetch
を使用してサーバーに変更を送信します。- 再度
mutate
を使用してキャッシュを再検証し、最新のデータを取得します。
これにより、ユーザーは変更が即座に反映されるように感じつつ、バックエンドとのデータも同期され、最新の状態が保持されます。
8. Paginationの実装
データの量が多い場合、一度にすべてのデータをロードするのではなく、ページネーションを使用するのが一般的です。SWRはページネーションの実装を簡単にします。
hooks/useTodosWithPagination.ts
javascriptCopy codeimport useSWR from 'swr';
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
export const useTodosWithPagination = (page) => {
const { data, error } = useSWR(`/api/todos?page=${page}`, fetcher);
return {
todos: data,
isLoading: !error && !data,
isError: error
};
};
components/PaginatedTodoList.tsx
javascriptCopy codeimport { useState } from 'react';
import { useTodosWithPagination } from '../hooks/useTodosWithPagination';
const PaginatedTodoList = () => {
const [page, setPage] = useState(1);
const { todos, isLoading, isError } = useTodosWithPagination(page);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading todos</div>;
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</button>
<button onClick={() => setPage(page + 1)}>Next</button>
</div>
);
};
export default PaginatedTodoList;
これにより、ページネーションを使ってToDoリストを表示することができます。
9. プリフェッチによる高速化
プリフェッチ (prefetch) は、通常、ユーザーが特定のデータを要求する前に、そのデータを事前にフェッチするテクニックです。これにより、ユーザーがデータにアクセスする際に、データが既にキャッシュされているため、レスポンスが速くなります。
SWR(stale-while-revalidate)ライブラリは、React プロジェクトでデータのフェッチを簡単に行うためのライブラリです。SWRはプリフェッチをサポートしており、以下の方法でプリフェッチを行うことができます。
SWRを使用したプリフェッチの例:
- useSWR フックを使ってプリフェッチする。
import useSWR from 'swr'
function MyComponent() {
// useSWR フックを使ってデータをフェッチする
const { data } = useSWR('/api/data', fetcher)
// ...
}
- trigger 関数を使ってプリフェッチする。これは、特定のキーに対してデータの再検証をトリガーします。
import { trigger } from 'swr'
// '/api/data' というキーでプリフェッチをトリガー
trigger('/api/data')
これを使って、たとえばマウスオーバーなどのユーザーのアクションに基づいてプリフェッチを行うことができます。
<button
onMouseOver={() => trigger('/api/data')}
>
Hover me to prefetch!
</button>
- mutate 関数を使ってキャッシュを手動で更新する。これは通常、データの更新後にキャッシュを同期させるために使われますが、プリフェッチとしても使えます。
import { mutate } from 'swr'
// 手動で '/api/data' というキーのキャッシュを更新
mutate('/api/data', newData)
これらの方法を使用して、SWRを用いて効果的にプリフェッチを行い、アプリケーションのパフォーマンスを向上させることができます。
10. まとめ
Next.js 13のappディレクトリを使用することで、プロジェクトの構造がより整理され、管理しやすくなります。SWRを活用することで、データフェッチングを効率的に行い、ユーザーエクスペリエンスを向上させることができます。React Queryと比較して、SWRはRESTful APIに特化した設計がされており、シンプルなAPIを使って効果的なキャッシュ戦略を実現します。
これらのツールを使用して、次世代の高性能なWebアプリケーションを構築しましょう。
コメント