【React】React イチから学習《発展編》

データフェッチングとパフォーマンス最適化

useSWRフック: データのキャッシングと再検証の最適化

useSWRは、Next.jsチームが開発したデータ取得ライブラリで、キャッシュ戦略とリアルタイムデータ更新のバランスを取りながら、パフォーマンスを最適化することに特化しています。

SWRとは、Stale-While-Revalidateの略で、「古いデータを表示しつつバックグラウンドで新しいデータを再取得する」という戦略に基づいています。

useSWRの特徴は以下の通りです。

  • キャッシュされたデータを即時表示し、ユーザーに高速なレスポンスを提供
  • バックグラウンドで新しいデータを取得し、常に最新の状態を保つ
  • キャッシュが自動的に管理されるため、開発者が手動で処理する必要がない

SWRの基本的な使い方

基本的なuseSWRの使い方はシンプルです。

import useSWR from 'swr';

// データ取得関数
const fetcher = (url) => fetch(url).then(res => res.json());

const MyComponent = () => {
  const { data, error, isLoading } = useSWR('/api/data', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load data</div>;

  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};

この例では、useSWRフックにAPIエンドポイントとデータ取得関数(fetcher)を渡しています。useSWRはデータがキャッシュされていればそれを即時に返し、無ければフェッチを行います。フェッチ後もキャッシュに保存され、次回からはキャッシュが使われるため、パフォーマンスが向上します。

useSWRと他のデータ取得手法の比較

useSWRfetchaxiosのような純粋なデータ取得手段とは異なり、キャッシングと再検証機能が組み込まれています。

種別 説明
fetch 標準的なブラウザのAPI。軽量で直接的だが、キャッシングやエラーハンドリングは手動で行う必要がある。
axios fetch よりも機能豊富で、リクエストの設定やレスポンスの処理が容易。が、キャッシュ管理は無い。
useSWR デフォルトでキャッシュを管理し、Stale-While-Revalidate戦略を提供。リアルタイム性が求められるアプリに最適。

useSWRはキャッシュ管理やリアルタイム更新が組み込まれているため、パフォーマンスを重視したい場面や複数回同じデータをフェッチする場合には、fetchaxiosよりも効率的です。

キャッシュ戦略とパフォーマンス向上のテクニック

useSWRでは、以下のようなキャッシュ戦略を使用してパフォーマンスを向上させることができます。

Stale-While-Revalidate

キャッシュされたデータを即時に返し、バックグラウンドで新しいデータを取得する戦略です。この戦略により、ユーザーには常に素早くデータを提供しつつ、最新のデータもバックグラウンドで更新できます。

Prefetching

予めデータを取得してキャッシュに保存しておくことで、必要なときに即座に表示できます。例えば、ページ遷移前に次のページのデータをフェッチしておくことで、ユーザーの待ち時間を大幅に減らせます。

useSWR('/api/data', fetcher, { prefetch: true });

Revalidation

データが古くなった際、または一定時間経過後に自動でデータを再フェッチすることで、常に最新の情報を表示します。

useSWR('/api/data', fetcher, { refreshInterval: 5000 });

依存関係のキャッシュ

複数のデータが相互に依存している場合、一度取得したデータを他のリクエストでも再利用することができます。

カスタムフックの作成: コードの再利用性を高めるフックの設計

プロジェクトが進行するにつれて、同じデータ取得処理が複数のコンポーネントで必要になる場合があります。こうした重複を防ぐために、カスタムフックを作成して、データフェッチロジックを一箇所にまとめることができます。

import useSWR from 'swr';

const useUserData = (userId) => {
  const fetcher = (url) => fetch(url).then(res => res.json());
  const { data, error, isLoading } = useSWR(`/api/user/${userId}`, fetcher);

  return {
    userData: data,
    isLoading,
    error,
  };
};

const UserProfile = ({ userId }) => {
  const { userData, isLoading, error } = useUserData(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load user data</div>;

  return (
    <div>
      <h1>{userData.name}</h1>
      <p>{userData.email}</p>
    </div>
  );
};

この例では、useUserDataというカスタムフックを作成して、ユーザーデータを取得するロジックをカプセル化しています。これにより、複数のコンポーネントで同じデータ取得処理を使う場合でも、コードの再利用性が向上し、保守が容易になります。

コンテキストとグローバルステート管理

useContextフック: グローバルステートの管理とProps Drillingの解決

Reactでは、コンポーネントツリー内で親から子へとデータを渡す方法として「Props」が使われます。しかし、親子関係が深くなると、必要のない中間のコンポーネントにもデータを渡す必要があり、コードが複雑化します。この問題を「Props Drilling」と呼びます。

useContextフックを使うと、Props Drillingの問題を解決し、複数のコンポーネント間でグローバルに状態を管理・共有できます。

import React, { createContext, useContext, useState } from 'react';

// コンテキストの作成
const UserContext = createContext();

const App = () => {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  return (
    // Context.Providerでグローバルなステートを提供
    <UserContext.Provider value={user}>
      <ChildComponent />
    </UserContext.Provider>
  );
};

const ChildComponent = () => {
  // useContextフックでコンテキストの値を取得
  const user = useContext(UserContext);

  return (
    <div>
      <p>User Name: {user.name}</p>
      <p>User Age: {user.age}</p>
    </div>
  );
};

export default App;

この例では、UserContextを使用することで、親コンポーネントのAppから直接子コンポーネントのChildComponentへステートを渡しています。中間のコンポーネントを経由せずにデータを受け渡すことができ、Props Drillingを避けることが可能です。

Context APIの実践: 小規模アプリでのステート共有方法

小規模なアプリケーションでは、複雑なステート管理ツールを使わずに、ReactのContext APIを使うことで十分なケースが多いです。たとえば、テーマや言語設定、ユーザー情報など、アプリ全体で共通して使用されるデータを扱う場合に適しています。

以下は、テーマ(ダークモード/ライトモード)の切り替えをContext APIで実現する例です。

import React, { createContext, useContext, useState } from 'react';

// テーマのContext作成
const ThemeContext = createContext();

const App = () => {
  const [theme, setTheme] = useState("light");

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <ThemedComponent />
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ background: theme === "light" ? "#fff" : "#333", color: theme === "light" ? "#000" : "#fff" }}>
      <p>Current Theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
};

export default App;

この例では、ThemeContextを作成し、アプリ全体でテーマの情報を共有しています。テーマの状態とその切り替え関数をThemeContext.Providerを通じて子コンポーネントに提供し、useContextフックでその値を取得してUIに反映させています。

Context APIはシンプルで強力なツールですが、コンテキストが増えるとコードが複雑になりやすい点には注意が必要です。そのため、状態管理がより複雑な大規模アプリケーションでは、次に紹介するReduxなどの専用ツールが有効です。

ReduxとReactの連携: 大規模アプリケーションにおけるグローバルステート管理

大規模なアプリケーションになると、状態管理がさらに複雑になります。複数の機能が互いに影響し合い、異なる部分の状態を管理するためにContext APIだけでは限界を感じることがあるでしょう。こうした場合に使われるのが、Reduxです。

Reduxは、全ての状態を1つの「ストア(Store)」で一元管理し、アプリケーション全体に統一的な方法で状態を渡すことができる状態管理ライブラリです。

Reduxの基本的な流れ

  • Action: 状態を変更するための"何をするか"の情報を定義。
  • Reducer: Actionに応じて、どのように状態を変化させるかを定義する関数。
  • Store: 現在の状態とReducerを保持し、アプリ全体でアクセス可能にする場所。
npm install redux react-redux

ReduxをReactに組み込む基本的な例です。

import React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 初期状態
const initialState = { count: 0 };

// Action
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });

// Reducer
const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// Store
const store = createStore(counterReducer);

const Counter = () => {
  // Reduxの状態を取得
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
};

const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

export default App;

この例では、countという単純な状態をReduxで管理し、useSelectorフックで状態を取得、useDispatchフックで状態変更のアクションをディスパッチしています。Providerコンポーネントを使うことで、ReduxのstoreをReactコンポーネント全体に渡しています。

Reduxは、アプリケーションの複数箇所で状態の変更が必要な場合や、異なるモジュール間でのステート管理が複雑化する場合に非常に有効です。また、デバッグや状態履歴の管理がしやすいというメリットもあります。

イベントハンドリングとフォーム管理

イベントハンドリングの基本: イベントリスナーの設定と管理

イベントハンドリングとは、ユーザーがUI上で行ったアクション(クリック、入力、フォーム送信など)に対して、適切な処理を行うことです。

Reactでは、HTMLと同様にイベントリスナーを設定できますが、イベント名がキャメルケースで記述されるという違いがあります。

function Example() {
  const handleClick = () => {
    alert("ボタンがクリックされました");
  };

  return <button onClick={handleClick}>クリック</button>;
}

Reactのイベントはシンセティックイベントと呼ばれ、ブラウザ間での互換性を保証し、React独自のイベントシステムによって管理されています。

よく使われるイベントは以下の通りです。

  • onClick: クリックイベント
  • onChange: フォーム要素の変更イベント
  • onSubmit: フォーム送信時のイベント
  • onKeyDown: キーボードのキーが押された時のイベント

イベントハンドリングの際に注意すべき点として、関数の直接呼び出しに注意が必要です。イベントハンドラに渡す関数は直接実行しないようにし、関数の参照を渡します。

// 間違い:
<button onClick={handleClick()}>クリック</button>

// 正しい:
<button onClick={handleClick}>クリック</button>

useReducerフック: 複雑なフォームやステート管理のためのリデューサーパターン

複雑なフォームやステート管理が必要な場合、useReducerフックが非常に便利です。

useReducerは、useStateの代わりにステート管理を行うためのフックで、リデューサーパターンに基づいています。リデューサーは、現在のステートとアクションを受け取り、新しいステートを返す純粋な関数です。

const [state, dispatch] = useReducer(reducer, initialState);

次に、複数の入力フィールドを持つフォームをuseReducerで管理する例を見てみましょう。

const initialState = {
  username: "",
  email: "",
  password: ""
};

function reducer(state, action) {
  switch (action.type) {
    case "SET_FIELD":
      return {
        ...state,
        [action.field]: action.value
      };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

function SignupForm() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleChange = (e) => {
    dispatch({
      type: "SET_FIELD",
      field: e.target.name,
      value: e.target.value
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // フォームデータ送信処理
    console.log(state);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={state.username}
        onChange={handleChange}
        placeholder="ユーザー名"
      />
      <input
        name="email"
        value={state.email}
        onChange={handleChange}
        placeholder="メールアドレス"
      />
      <input
        name="password"
        value={state.password}
        onChange={handleChange}
        placeholder="パスワード"
        type="password"
      />
      <button type="submit">送信</button>
    </form>
  );
}

このように、useReducerは複数のステートを1つのリデューサー関数で一元管理できるため、複雑なフォームやステートロジックが必要な場面で有効です。

react-hook-formライブラリ: フォーム状態管理の効率化

フォーム管理を効率的に行うためのライブラリとして、react-hook-formが広く利用されています。このライブラリは、Reactの標準的なコンポーネント設計に沿った形でフォーム管理を行い、非常に軽量でパフォーマンスも高いです。また、フォームバリデーションやエラーメッセージの表示も簡単に実装できます。

まず、react-hook-formをインストールします。

npm install react-hook-form

次に、useFormフックを使ってフォームをセットアップします。

import { useForm } from "react-hook-form";

function ContactForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("name", { required: true })}
        placeholder="名前"
      />
      {errors.name && <span>名前は必須です</span>}

      <input
        {...register("email", {
          required: "メールアドレスは必須です",
          pattern: {
            value: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,4}$/i,
            message: "無効なメールアドレスです"
          }
        })}
        placeholder="メールアドレス"
      />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit">送信</button>
    </form>
  );
}
  • バリデーション: 入力フィールドに対して簡単にバリデーションルールを設定でき、エラーがある場合には即座にエラーメッセージを表示できます。
  • 軽量・高パフォーマンス: フォームデータをHTMLのネイティブなフォーム要素で直接管理するため、ステート管理に比べてパフォーマンスが向上します。
  • リファクタリングの容易さ: 入力フィールドが増えても、各フィールドをregisterメソッドで簡単に追加できるため、コードの保守が容易です。

関連記事

【React】React イチから学習《基礎編》
# Reactの基本 ## Reactとは? Reactは、Facebookによって2013年にリリースされたフロントエンドライブラリで、主にユーザーインターフェース (UI) の構築に使われます。Reactの特徴は、「コンポーネント指向の [...]
2024年10月21日 16:43
強力なUIコンポーネントライブラリ「MUI(Material-UI)」について《トラブルシューティング編》
# よくある質問とトラブルシューティング MUI(Material-UI)を使用していると、初心者でも経験者でも共通して直面しやすいエラーや問題がいくつか存在します。この章では、よく発生するエラーとその解決方法、ドキュメントでは見つけにくい設定の [...]
2024年10月15日 13:41
強力なUIコンポーネントライブラリ「MUI(Material-UI)」について《テーマ・スタイリング編》
# MUIのテーマカスタマイズ MUI(Material-UI)はデフォルトのテーマをベースに、簡単にテーマをカスタマイズできる柔軟なシステムを提供しています。これにより、アプリケーションのデザインをブランドや要件に合わせて調整可能です。 [...]
2024年10月15日 11:56
強力なUIコンポーネントライブラリ「MUI(Material-UI)」について《コンポーネント編》
# 基本的なコンポーネントの使用方法 MUIは、豊富なUIコンポーネントを提供しており、それを使うことでReactアプリケーションの開発を迅速に進められます。 ここでは、MUIでよく使用される基本的なコンポーネントであるボタン、テキストフィ [...]
2024年10月15日 10:32
強力なUIコンポーネントライブラリ「MUI(Material-UI)」について《概要編》
# はじめに ウェブアプリケーションの開発において、デザインと使いやすさを両立することは非常に重要です。MUI(Material-UI)は、Reactと組み合わせて簡単に洗練されたユーザーインターフェースを構築できる強力なUIコンポーネントライブ [...]
2024年10月15日 10:10
【React】React-ToastifyのカラーテーマとPCの外観モードを連動させる方法【TypeScript】
# はじめに Webアプリでユーザー情報の変更などを行った際、処理が正常に完了したことをユーザーに伝えるために、画面の端に一時的にメッセージを表示する機能のことを「トースト」といいます。 >トーストとは、主にデスクトップアプリケーションの機 [...]
2022年7月31日 21:24
RailsアプリにおけるReactの使い方や注意点など
# はじめに [こちらのページ](https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048)でReactの基本について勉強しました。 Railsアプリの中でReactを利用するとい [...]
2019年12月13日 12:37
【Rails】Railsアプリにreact-railsを追加する手順
# はじめに JavaScriptフレームワークといえば長らくjQuery一強でした。しかし、ここ数年はAngularJSやReact、Vue.jsといった新しいフレームワークがどんどんと登場しています。過去5年間の検索回数の推移を見てみると、2 [...]
2019年12月11日 15:21