logo

SWRのkeepPreviousDataでユーザー体験を改善する実装ガイド

2025-07-18
21 days ago

開発環境

  • React 18.x
  • TypeScript 5.x
  • SWR 2.2.x
  • Next.js 14.x (App Router)

前提

読者の想定レベル

  • Reactの基本的な使い方を理解している
  • SWRの基本的な使い方(useSWRフック)を知っている
  • TypeScriptの基本構文を理解している

この記事で扱うこと

  • SWRのkeepPreviousDataオプションの使い方
  • 従来の実装パターンとの比較
  • 実際のコード例とデモ

この記事で扱わないこと

  • SWRの基本的な導入方法
  • React Queryなど他のデータフェッチライブラリとの比較

本題

検索機能でよくあるUXの課題

多くのWebアプリケーションで、検索機能を実装する際に以下のような問題に遭遇したことはありませんか?

  • 検索のたびに画面がちらついて見づらい
  • 検索中にローディング状態になって、前の結果が見えなくなる
  • ユーザーが連続して検索しにくい体験になってしまう

これらの問題は、従来のよくある実装パターンに起因しています。

従来の実装パターンの問題点

一般的に、SWRを使った検索機能は以下のように実装されることが多いです。

function SearchPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const { data, error, isLoading } = useSWR(
    `search-${searchQuery}`,
    () => fetchSearchResults(searchQuery)
  );

  // よくある実装パターン
  if (isLoading) {
    return <div>検索中...</div>;
  }

  if (error) {
    return <div>エラーが発生しました</div>;
  }

  return (
    <div>
      <input 
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="検索..."
      />
      {/* 検索結果の表示 */}
      {data?.map(item => <Item key={item.id} {...item} />)}
    </div>
  );
}

この実装の問題点は・・

  1. 画面全体がリセットされる: 検索のたびにif (isLoading)によって画面全体がローディング状態になる
  2. 前の結果が見えなくなる: 新しい検索結果が取得されるまで、前の結果が完全に消える
  3. 視覚的な継続性がない: データの変化が唐突で、ユーザーが迷いやすい

解決策:keepPreviousDataの活用

SWRのkeepPreviousDataオプションを使うことで、これらの問題を簡単に解決できます。

そしてもう一点、注意点としてif (isLoading)をやめることです。

function ImprovedSearchPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const { data, error, isLoading } = useSWR(
    `search-${searchQuery}`,
    () => fetchSearchResults(searchQuery),
    {
      keepPreviousData: true, // ← これを追加するだけ!
    }
  );

  return (
    <div>
      <input 
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="検索..."
      />
      
      {/* ローディング状態を別途表示 */}
      {isLoading && (
        <div className="loading-indicator">検索中...</div>
      )}
      
      {/* 前のデータを保持しながら新しいデータを表示 */}
      {data?.map(item => <Item key={item.id} {...item} />)}
    </div>
  );
}

keepPreviousDataの動作原理

keepPreviousData: trueを設定すると、SWRは以下のように動作します。

  1. 初回データ取得: 通常通りローディング状態になり、データを取得
  2. 2回目以降のデータ取得: 新しいデータが取得されるまで、前のデータを保持し続ける
  3. 新しいデータの取得完了: 前のデータから新しいデータにスムーズに切り替わる

実装例:検索機能付きユーザーリスト

実際の実装例を見てみましょう。

interface User {
  id: number;
  name: string;
  email: string;
  department: string;
}

const fetchUsers = async (query: string): Promise<User[]> => {
  // APIコールをシミュレート
  await new Promise(resolve => setTimeout(resolve, 800));
  
  // 検索ロジック
  if (!query) return allUsers;
  return allUsers.filter(user => 
    user.name.includes(query) || 
    user.email.includes(query) || 
    user.department.includes(query)
  );
};

export default function UserSearchPage() {
  const [searchQuery, setSearchQuery] = useState('');

  const { data: users, error, isLoading } = useSWR(
    searchQuery ? `users-${searchQuery}` : 'users',
    () => fetchUsers(searchQuery),
    {
      keepPreviousData: true,
      revalidateOnFocus: false,
    }
  );

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">ユーザー検索</h1>
      
      {/* 検索フォーム */}
      <div className="mb-6">
        <input
          type="text"
          placeholder="ユーザーを検索(名前、メール、部署)"
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </div>

      {/* ローディング状態の表示 */}
      {isLoading && (
        <div className="mb-4 text-sm text-blue-600">
          🔍 検索中...
        </div>
      )}

      {/* エラー処理 */}
      {error && (
        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
          エラーが発生しました: {error.message}
        </div>
      )}

      {/* ユーザーリスト */}
      <div className="grid gap-4">
        {users && users.length > 0 ? (
          users.map((user) => (
            <div key={user.id} className="bg-white p-4 border border-gray-200 rounded-lg shadow-sm">
              <div className="flex justify-between items-start">
                <div>
                  <h3 className="font-semibold text-lg">{user.name}</h3>
                  <p className="text-gray-600">{user.email}</p>
                </div>
                <span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded">
                  {user.department}
                </span>
              </div>
            </div>
          ))
        ) : (
          <div className="text-center py-8 text-gray-500">
            ユーザーが見つかりませんでした
          </div>
        )}
      </div>
    </div>
  );
}

実際に体験してみよう

以下のデモで、keepPreviousDataの効果を実際に体験できます。

両方のデモで同じ検索を行って、ユーザー体験の違いを比較してみてください。

さいごに

SWRのkeepPreviousDataは、わずか一行のオプション追加でユーザー体験を改善できる強力な機能です。

特に検索機能やフィルタリング機能において、従来の「ローディング中は画面全体をリセット」するパターンから脱却し、より自然で使いやすいインターフェースを提供できます。

実装も非常に簡単なので、既存のプロジェクトにも容易に導入できます。ぜひ今回紹介したデモを試して、その効果を実感してみてください。

ユーザーにとって快適な検索体験を提供することで、アプリケーションの使いやすさが大幅に向上するはずです。

参照