フリーランスWebエンジニア HTNCode

▲
  • HOME
  • ABOUT
  • SERVICE
  • ACHIEVEMENT
  • BLOG
  • CONTACT
  • githubIcon
  • twitterIcon
プライバシーポリシー
footerLogoImage

© HTNCode All Rights Reserved.

eyecatch

ReactでのGlobal State管理まとめ

作成日時:2024年7月1日 02:48

更新日時:2025年5月13日 02:24

React

Next.js

はじめに

  

(これは2023年11月に投稿した記事の再投稿です。諸事情により記事閲覧ができなくなっていたため修正しました)

HTNCodeです。
ReactにおけるGlobal State管理の方法について、useSWRを含めた方法をまとめておこうと思ったのでここに残します。

 

 

props(バケツリレー注意)

  

まずはpropsで渡す方法。

  

// page.tsx

"use client";
import State from "./State";
import { useState } from "react";

export interface Props {
  value: string;
}

export default function Home() {
  const [sampleState] = useState("この文字はpropsで渡しました");

  return (
    <main>
      <h1>ReactにおけるGlobal state管理方法</h1>
      <State value={sampleState} />
    </main>
  );
}

 

// State.tsx
import React from "react";
import { Props } from "./page";

const State = (props: Props) => {
  return (
    <div>
      <h2>Propsで管理</h2>
      {props.value}
    </div>
  );
};

export default State;

 

<結果>

きちんと渡されますが、階層が深くなるにつれ、いわゆるバケツリレーが発生します。
小規模な開発ならまだしも、中規模以上なら他の方法を検討した方がよいでしょう。

 

 

useContextを使った管理方法

  

React標準の機能であるuseContextを使うパターン。
以下の手順で利用できます。

1.createContext()を使ってコンテキストを作成

 

// MyContext.tsx

import { createContext } from "react";

export const MyContext = createContext<string | null>("この文字は初期値です");

 

2.コンテキストプロバイダーに値を渡す

※追記:React19で.Providerを省略できるようになりました。

// page.tsx

"use client";
import State from "./State";
import { useState } from "react";
import { MyContext } from "./MyContext";

export default function Home() {
  const [sampleText] = useState("この文字はuseContextで渡しました");

  return (
    <main>
      <h1>ReactにおけるGlobal state管理方法</h1>
      {/* コンテキストプロバイダーに値を渡す */}
      <MyContext value={sampleText}>
        <State />
      </MyContext>
    </main>
  );
}

 3.useContext()を使ってデータを取得

 

// State.tsx

import React, { useContext } from "react";
import { MyContext } from "./MyContext";

const State = () => {
  const sampleText: string | null = useContext(MyContext);

  return (
    <div>
      <h2>useContextで管理</h2>
      <p>{sampleText}</p>
    </div>
  );
};

export default State;

 

<結果>

「この文字はuseContextで渡しました」と表示され、きちんと渡せていることが確認できます。
注意点として、コンテキストの箱に入っている値が複数の場合、そのうちどれか一つでも更新されると、
そのコンテキストが紐づく全コンポーネントが再レンダリングされます。
必要最低限の再レンダリングにするために、複数のコンテキストを作る、もしくはmemo化をして管理するようにしましょう。

※タブなどによるコンポーネント表示のだし分けを親コンポーネントが行っている場合、現在レンダリングされていない(タブ表示されていない)子コンポーネントは、仮にすべての子コンポーネントがcontextを購読していたとしても、コンポーネントツリーに表示されていないコンポーネントがマウントされていない状態となるため、再レンダリングされることはありません。
 つまり、複数のコンテキストを作ったり、memo化を行わなくても副次効果的に無駄な再レンダリングを防ぐことができますが、それはあくまで副次効果にすぎず、安全な実装ではありません。
 バグの原因となるため、useContextを使う部分はそれのみで再レンダリングに対するコントロールをきちんとしておくことが大切です。

 

 

Reduxを使った管理方法

 

useContextのようにPropsで値を受け渡すことなくState管理する方法の一つです。
2015年から存在しているMeta社提唱のFluxアーキテクチャに則って設計されている状態管理のライブラリであり、
後述するRecoilの登場まではデファクトスタンダードに近いポジションを取っていたものです。
特に大規模なプロジェクトに適しているといわれています。
単一方向にしかデータが流れない、というところが特徴。

 

■Redux公式サイト
https://redux.js.org/

※参考図:公式サイトから引用

1.Store・・・アプリケーション全体の状態を保持する場所で、単一のオブジェクトです。これにより、状態を一元管理できます。
2.Action・・・アプリケーション内で何かが起こったことを示すオブジェクトです。アクションは状態の変更のトリガーとして機能し、必要な情報を含みます。
3.Reducer・・・アクションを受け取り、新しい状態を生成する関数です。リデューサーは純粋な関数であるため、同じ入力に対しては常に同じ出力を生成します。
4.Dispatch・・・ アクションを送信して、リデューサーによる状態の更新をトリガーします。これにより、アプリケーションの状態が変更されます。

 

Reduxを利用するには、reduxとreact-reduxをインストールします。
@reduxjs/toolkitには、Reduxのコアパッケージと、Reduxアプリの構築に不可欠な主要パッケージが含まれています。
react-reduxはreactのコンポ-ネントからreduxにアクセスするためのライブラリです。

  

npm install @reduxjs/toolkit react-redux

 

※単体でインストールするなら以下。

  

npm install redux react-redux

 

storeを作成。reducerは後ほど作る。
また、今回TypeScriptを使用しているため、RootState型を定義してエクスポートしておきます。

  

// MyStore.ts

import { configureStore } from "@reduxjs/toolkit";
import textReducer from "./reducers/textSlice";

const store = configureStore({
  reducer: {
    text: textReducer,
  },
});
export default store;

// RootState型を定義してエクスポート
export type RootState = ReturnType<typeof store.getState>;

 

次に、状態をSliceに定義し、Reducerを作成します。

 

// textSlice.tsx

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  message: "この文字は初期値です",
};

const textSlice = createSlice({
  name: "sampleText",
  initialState,
  reducers: {
    setMessage: (state, action) => {
      state.message = action.payload; // メッセージを設定するReducerを作成
    },
  },
});

export const { setMessage } = textSlice.actions; // 新しいReducerをエクスポート
export default textSlice.reducer;

※補足:

 ・State: 状態を保持するオブジェクトで、アプリの使用中に値は変化します。

 ・Action:更新の内容やタイプを指示するオブジェクトです。

 ・Reducer:Stateをアップデートする関数です。引数としてstateとactionを持ちます。

 ・Slice:Redux Toolkitに含まれる状態のデフォルト値やアクションを定義するオブジェクトです。

 

呼び出していきます。Providerタグでコンポーネントを囲むことでRedux Storeが使えるようになります。

  

// page.tsx

import State from "./State";
import { Provider } from "react-redux";
import store from "./MyStore";

export default function Home() {
  return (
    <main>
      <h1>ReactにおけるGlobal state管理方法</h1>
      {/* Providerタグでコンポーネントを囲むことでRedux Storeが使える */}
      <Provider store={store}>
        <State />
      </Provider>
    </main>
  );
}

 

使いたいコンポーネント側で呼び出します。

  

// State.tsx

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { setMessage } from "./reducers/textSlice";
import { RootState } from "./MyStore"; //型をインポート

export default function State() {
  const message = useSelector((state: RootState) => state.text.message);
  const dispatch = useDispatch();

  const handleClick = () => {
    dispatch(setMessage("この文字はReducerを使って渡しました"));
  };

  return (
    <div>
      <h2>Stateコンポーネント</h2>
      <p>メッセージ: {message}</p>
      <button onClick={handleClick}>ボタンをクリック</button>
    </div>
  );
}

 

これでボタンをクリックすると「メッセージ:この文字はReducerを使って渡しました」と表示されるはずなのですが、
依存関係のエラーが出て使えなかったので、動作確認はできておりません。
万が一誤り等あれば、CONTACTページからご指摘いただけますと幸いです。

 

ちなみに、ここでは解説しませんが、今はredux-react の useSelector Hooksを利用することができるようです。
useSelectorに置き換えることでコードがスッキリします。

 

 

Recoilを使った管理方法

Meta社が2020年に公開した新しい状態管理のライブラリです。
Reduxのような学習コストがかかるものではなく、useStateと同じような感覚で使用することが可能です。
Reduxと同様にFluxというアーキテクチャに則って設計されており、Atom や Selector と呼ばれるものを使用して管理を行います。

 

まずはインストールします。

 

npm i recoil

 

続いて、atomを設定します。

  

//MyAtom.tsx

import { atom } from "recoil";

export const MyText = atom({
  key: "MyText",
  default: "この文字はRecoilを使って渡しました",
});

 

早速使えるようにしていきます。

 

// page.tsx

"use client";
import { RecoilRoot } from "recoil";
import State from "./State";

export default function Home() {
  return (
    <main>
      <h1>ReactにおけるGlobal state管理方法</h1>
      {/* RecoilRootタグでコンポーネントを囲むことで使えます */}
      <RecoilRoot>
        <State />
      </RecoilRoot>
    </main>
  );
}

 

きちんと表示できました。

 

 

※2024/7/1追記:
layout.tsxは基本サーバーコンポーネントであるため、ページ丸ごとで状態管理したい場合に、layout.tsxでRecoilRootを使用したいケースがよく発生します。
そういった時は、以下のようにWrapperとなるコンポーネントを作成して、それをlayout.tsxでインポートして使用することで、クライアントコンポーネントとサーバーコンポーネントをきっちり分けることができます。
ご参考まで。

"use client";
import { RecoilRoot } from "recoil";

const RecoilRootWrapper = ({ children }: { children: React.ReactNode }) => {
  return <RecoilRoot>{children}</RecoilRoot>;
};

export default RecoilRootWrapper;

 

useSWRを使った管理方法

 

useSWRは、Next.jsを開発しているVercelが開発した、Reactアプリケーションでデータの取得とキャッシュを管理するためのカスタムフックライブラリです。
useSWRは、データを非同期的にフェッチ、キャッシュし、必要に応じて再利用することができます。
これにより、APIリクエストやクエリの結果を簡単に管理し、アプリケーションのパフォーマンスを向上させることができます。

 

まずuseSWRフックの使い方から。
以下のようにuseSWRを使用することで、データのフェッチとキャッシュ、エラーハンドリング、ローディング表示などを簡単に管理できます。

 

import useSWR from 'swr';

function fetchData(url) {
  const { data, error } = useSWR(url, fetch); // URLをキーとしてデータを取得

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

  if (!data) {
    return <div>データを読み込んでいます...</div>;
  }

  return <div>データ: {data}</div>;
}

 

状態管理のためにuseSWRを使用したい場合、第2引数の fetcherを渡す箇所に対して、nullを渡します。
これにより、useSWRを使ったGlobal Stateの管理が行えるようになります。

 

  const { data:sampleText, error:setSampleText } = useSWR(url, sampleText, null, { initialData: 'この文字は初期値です' });

 

useSWRはmutate関数を使用することでデータを更新できるので、これによってuseStateのような使い方ができるというわけです。
また、useSWRの第3引数には様々なoptionを指定できるので、initialDataの部分でデータの初期値を設定しておくことができます。

(追加)Zustandを使った管理方法

Zustandは状態管理のためのライブラリで、useContextのContextの値が変更されると、すべてのコンポーネントが再レンダリングされてしまう問題を回避できる一つの方法です。

ストアを準備して、使いたいコンポーネントで使うだけ。公式にも書かれている通りReduxに似ていますが、それよりもさらに簡単に使うことができます。

 

■Zustand公式サイト

https://zustand.docs.pmnd.rs/getting-started/introduction

 

まずはストアを作成します。

// /store/useStore.tsx

import { create } from "zustand";

interface StoreState {
  content: string;
}

const useStore = create<StoreState>(() => ({
  content: "Zustandで本文を取得しました"
}));

export default useStore;

そして、使いたいコンポーネントで使います。 

"use client";

import useStore from "./store/useStore";

export default function Home() {
  const { content } = useStore();

  return (
    <div>
      <p>{content}</p>
    </div>
  );
}

おわりに

他にもReduxの代替え手段としてRecoil以外にも、useReducerを使う方法がありますし、
GraphQL APIをクライアント側で効率よく操作するためのライブラリApollo Clientを利用したState管理なんかもあります。
どのような開発環境でどの技術選定をするのか、状況に応じて使い分けをしていきましょう。

いつか振り返った時に役立つことを祈りつつ、まとめておこうまとめておこうと思いながら、
ずっと下書きにおいてものをやっと公開できました。笑
 

それではまた。

 

HTNCode