1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】グローバルステートとContext APIの使い方を整理してみた

1
Posted at

1. はじめに

今回は「挫折しないReactの教科書」の第5章を学習しました。
テーマはグローバルステートContext APIです。
propsでのデータ共有の限界と、それを解決するContext APIの使い方を整理しておきます。

2. propsのバケツリレーとその問題点

まずはpropsを使ったコンポーネント間のデータ共有の課題を学びました。

2.1 propsでのstate共有

propsを使うと、親コンポーネントのstateを子コンポーネントに渡すことができます。
以下はその基本的な例です。

親から子へstateをpropsで渡すコード:

// 親コンポーネント
function App() {
  // stateで選択された色を管理
  const [selectedColor, setSelectedColor] = useState("blue");

  return (
    // ColorDisplayコンポーネントにcolorという名前でselectedColorを渡す
    <ColorDisplay color={selectedColor} />
  );
}

// 子コンポーネント
function ColorDisplay(props) {
  // propsからcolorを取り出す
  const { color } = props;
  return <h1 style={{ color: color }}>選択された色: {color}</h1>;
}

1〜2層程度の構造であれば、この方法で問題ありません。
しかし、コンポーネントの階層が深くなると、問題が生じてくるみたいです。

2.2 バケツリレーのデメリット

大規模なアプリケーションでは、App → ComponentA → ComponentB → ComponentC のように5層以上になることがよくあるようです。
この深い階層でpropsを受け渡し続けると、以下の3つのデメリットが生じます。

propsが増えてコンポーネントの目的がわからなくなるという問題があります。
中間コンポーネントが本来の役割とは関係ないpropsを大量に持つことになり、そのコンポーネントが何をするものなのかが不明確になるみたいです。

コードが複雑になり保守性が悪化します。
propsの名前が変更されるたびに、経由するすべてのコンポーネントを修正する必要があり、どのデータがどこで使われているかの把握が難しくなるようです。

無駄な再レンダリングが発生するという問題もあります。
propsでデータが更新されると、そのデータを使わない中間コンポーネントも含めて、すべてのコンポーネントで処理の再実行が発生する可能性があり、パフォーマンスの低下につながるみたいです。

2.3 propsバケツリレーを体験するサンプル

この問題を実感するために、入力されたテキストを複数のコンポーネントで表示するサンプルを作りました。

stateを管理するApp.jsxの全体コードです:

import { useState } from 'react';
// コンポーネントをインポート
import WrapperA from './WrapperA';
import ComponentB from './ComponentB';

function App() {
  // カウントとテキストのstateを定義
  const [count, setCount] = useState(0);
  const [inputText, setInputText] = useState('');

  return (
    <>
      <div>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
        />
        {/* WrapperAコンポーネントにinputTextをpropsで渡す */}
        <WrapperA inputText={inputText} />
        {/* ComponentBコンポーネントにinputTextをpropsで渡す */}
        <ComponentB inputText={inputText} />
      </div>
    </>
  );
}

export default App;

中間コンポーネントであるWrapperA.jsxのコードです。
inputTextを自分では使わないのに、ComponentAへ渡すためだけにpropsとして受け取っています:

import ComponentA from './ComponentA';

function WrapperA({ inputText }) {
  // propsでinputTextを受け取る(WrapperA自体は使用しない)
  return (
    <div style={{ border: '1px solid #000', marginTop: '10px' }}>
      <p>WrapperA</p>
      {/* ComponentAコンポーネントにさらにinputTextを渡す */}
      <ComponentA inputText={inputText} />
    </div>
  );
}

export default WrapperA;

実際にinputTextを使用するComponentA.jsxComponentB.jsxのコードです:

// ComponentA.jsx
function ComponentA({ inputText }) {
  // propsでinputTextを受け取る
  return (
    <div>
      {/* やっとここでテキスト情報を使用 */}
      <p>ComponentAで表示中: {inputText}</p>
    </div>
  );
}

export default ComponentA;
// ComponentB.jsx
function ComponentB({ inputText }) {
  // propsでinputTextを受け取る
  return (
    <div>
      {/* ここでもテキスト情報を使用 */}
      <p>ComponentBで表示中: {inputText}</p>
    </div>
  );
}

export default ComponentB;

WrapperAinputTextを自分では使わないにもかかわらず、ComponentAへ渡すためだけに受け取っている点がバケツリレーの典型例だと理解しました。

3. グローバルステートとContext API

propsバケツリレーの問題を解決するために、グローバルステートとContext APIを学びました。

3.1 グローバルステートとは

グローバルステートとは、アプリケーション全体で共有されるstateのことです。
グローバルステートを使うと、必要なコンポーネントが直接データにアクセスでき、中間コンポーネントを経由する必要がなくなるみたいです。

ただし、グローバルステートはやたらめったら使えばいいものではないようです。
どこからでも変更できるため、「そのstateがいつ・どこで変更されるのか」「中身がどうなっているのか」を予測するのが難しくなるみたいです。
テーマカラーやログイン状態など、アプリケーション全体で本当に必要な情報に限定して使い、そうでない場合はuseStateを使うのがよいようです。

3.2 Context APIの使い方

ReactでグローバルステートをContext APIで実現するには、以下の4ステップで進めます。

  1. createContextでグローバルステートを扱う箱を作る
  2. グローバルステートにしたいデータをstateとして定義する
  3. providerでアプリケーション内でグローバルステートを使える範囲を設定する
  4. useContextでグローバルステートを使いたい箇所から呼び出して利用する

まず、App.jsxを書き換えてcontextを作成する全体コードです:

// contextの作成に使うcreateContextをインポート
import { createContext, useState } from 'react';
import WrapperA from './WrapperA';
import ComponentB from './ComponentB';

// テキスト情報のグローバルステートを扱うcontextを作成
export const TextContext = createContext();

function App() {
  const [inputText, setInputText] = useState('');

  return (
    // contextのproviderで囲んで、子コンポーネントからテキスト情報にアクセスできるようにする
    <TextContext.Provider value={{ inputText, setInputText }}>
      <div>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
        />
        {/* propsでstateを受け渡していた部分を削除 */}
        <WrapperA />
        <ComponentB />
      </div>
    </TextContext.Provider>
  );
}

export default App;

createContextの引数に何も渡していないため、初期値はundefinedとなるようです。
TextContext.Providervalueプロパティに{ inputText, setInputText }を渡すことで、providerで囲まれた子コンポーネントすべてがinputTextにアクセスしたり、setInputTextstateに値を入れたりできるようになるみたいです。

続いて、WrapperA.jsxを書き換えます。
propsで受け渡していた部分が不要になるので、シンプルになります:

import ComponentA from './ComponentA';

function WrapperA() {
  // propsでstateを受け取っていた部分を削除
  return (
    <div style={{ border: '1px solid #000', marginTop: '10px' }}>
      <p>WrapperA</p>
      {/* propsでstateを受け渡していた部分を削除 */}
      <ComponentA />
    </div>
  );
}

export default WrapperA;

次に、ComponentA.jsxでグローバルステートから直接データを取得します:

// グローバルステートを使うのに必要な部分をインポート
import { useContext } from 'react';
import { TextContext } from './App';

function ComponentA() {
  // propsではなく、グローバルステートから直接テキストの情報を取得
  const { inputText } = useContext(TextContext);

  return (
    <div>
      <p>ComponentAで表示中: {inputText}</p>
    </div>
  );
}

export default ComponentA;

同じように、ComponentB.jsxも書き換えます:

// グローバルステートを使うのに必要な部分をインポート
import { useContext } from 'react';
import { TextContext } from './App';

function ComponentB() {
  // propsではなく、グローバルステートから直接入力されているテキストの情報を取得
  const { inputText } = useContext(TextContext);

  return (
    <div>
      <p>ComponentBで表示中: {inputText}</p>
    </div>
  );
}

export default ComponentB;

Context APIを使うことで、ComponentAComponentBの両方が直接グローバルステートからテキストの情報を利用できるようになり、中間での受け渡しやpropsでの受け渡しに関連していた部分が不要になるみたいです。

3.3 Context APIの注意点

Context APIを使う際に、異なる種類のデータを一つのcontextにまとめて管理すると、コードの理解や修正が困難になるようです。

たとえば以下のように、ユーザー情報とテーマ情報を同じcontextに入れてしまう例が悪い例です:

// 悪い例:異なる種類のデータを一つのcontextにまとめてしまっている
const AppContext = createContext();

function App() {
  const [user, setUser] = useState({ name: "田 中", email: "tanaka@example.com" });
  const [theme, setTheme] = useState("light");

  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
      <Header />
      <UserProfile />
      <ThemeSelector />
      <MainContent />
    </AppContext.Provider>
  );
}

この場合、AppContextという名前からはどのようなデータが管理されているのかがわかりません。
関連するデータごとにcontextを分割するのが良い例です:

// 良い例:データの種類ごとにcontextを分割する
const UserContext = createContext();
const ThemeContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: "田 中", email: "tanaka@example.com" });
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <Header />
        <UserProfile />
        <ThemeSelector />
        <MainContent />
      </ThemeProvider>
    </UserProvider>
  );
}

contextを分けることで、ユーザー情報に関してはUserContext、テーマに関してはThemeContextを見ればよく、それぞれの役割が明確になるみたいです。

Context APIを使用する際は、関連するデータをまとめて管理し、異なる種類のデータは別々のcontextに分割することが重要です。

まとめ

今回はpropsのバケツリレー問題とContext APIの使い方を学びました。
コンポーネント設計を考えるうえで重要な概念だと感じたので、ここに整理しておきます。

今回の気づき

WrapperAが自分では使わないのにinputTextをpropsとして受け取っていたサンプルを見たとき、「これは確かに読みにくいな」と感じました。
Context APIを導入することで、WrapperAのコードが一気にシンプルになり、コンポーネントの役割が明確になることが実感できました。
ただし、グローバルステートは便利すぎるがゆえに乱用すると管理が難しくなるため、本当に全体で共有すべきデータにだけ使う、という判断が大事だと理解しました。

ハマりやすいポイント

  • createContextはファイルの外で定義してexportしておかないと、他のコンポーネントからimportできないので注意が必要です。
  • Providervalueに渡すデータを間違えると、useContextで取り出した際にundefinedになるので確認が必要です。
  • 異なる種類のデータを一つのcontextにまとめてしまうと、後から管理が難しくなるみたいです。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?