Next.js 13のappディレクトリとSWRを活用したデータフェッチの最適化

React

次世代のフロントエンド開発では、データの取得とその最適化が重要な要素です。この記事では、Next.js 13のappディレクトリを使用して、SWRというデータフェッチングライブラリを活用してデータの取得を最適化する方法を解説します。さらに、React Queryとの違いも考察します。

ぽん
ぽん

キャッシュベースのAPI通信ってなんだかすごそう!

ponta
ponta

キャッシュベースにすることで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 関数です。この関数内で以下の手順を踏んでいます。

  1. mutate を使用してオプティミスティック UI 更新を行い、ユーザーに即座に変更を表示します。
  2. fetch を使用してサーバーに変更を送信します。
  3. 再度 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を使用したプリフェッチの例:

  1. useSWR フックを使ってプリフェッチする。
import useSWR from 'swr'

function MyComponent() {
  // useSWR フックを使ってデータをフェッチする
  const { data } = useSWR('/api/data', fetcher)

  // ...
}
  1. trigger 関数を使ってプリフェッチする。これは、特定のキーに対してデータの再検証をトリガーします。
import { trigger } from 'swr'

// '/api/data' というキーでプリフェッチをトリガー
trigger('/api/data')

これを使って、たとえばマウスオーバーなどのユーザーのアクションに基づいてプリフェッチを行うことができます。

<button
  onMouseOver={() => trigger('/api/data')}
>
  Hover me to prefetch!
</button>
  1. 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アプリケーションを構築しましょう。

コメント