🐣

【Next.js】グローバルステートライブラリ比較

2024/06/07に公開

🔰はじめに

Next.jsを用いた開発において、グローバルステートに関するライブラリを決めるターンが来たので
色々使ってみた所感を記載

😵‍💫忙しい人のために

  • Context API含め7つ調査したよ
  • ここ で比較しとるよ

🔬そもそもグローバルステートって??

説明

コンポーネントを跨いだ情報の管理。
props で渡して管理する方法もあるが、コンポーネントが多くなったり ./pages/ 配下のコンポーネント同士の受け渡しが難しかったりで管理が煩雑になってしまうためグローバルステートを用いる。

グローバルステートライブラリには「アトムベース」と「ストアベース」という2つの大きな括りが存在している。

アトムベース・ストアベース

2つの違いはアプリケーションの情報全体を「小さく分けて管理する」か「大きくいっしょくたに管理する」か。
それぞれ優劣があるわけじゃなくてプロジェクトや開発チームによって決めると良さげ。

アトムベース ストアベース
📎 情報を小さい単位で分けて管理する 📎 情報を1つの大きな括りで管理する
✅ 1つの情報粒度が小さく分かりやすい
✅ 意図しない変更を削減できる

🚫 情報同士の依存関係が多いと管理が複雑
🚫 情報の数が多くなり大変
✅ 情報を一元管理できる
✅ サイト全体にまたがるデータ管理が容易

🚫 変更しない情報のやり取りが多くなる
🚫 情報の規模が大きくなり大変
⭐ 細かい操作が必要だが状態管理自体は単純な場合
⭐ パフォーマンスにこだわる場合
⭐ それぞれの状態に依存関係がなく独立している場合

⭐ 小〜中規模な開発の場合
⭐ 簡単に導入したい場合
⭐ 複雑な状態を管理する場合
⭐ 複雑な構造のデータを扱う場合
⭐ 状態を一元管理したい場合

⭐ 大規模な開発の場合
⭐ 開発者が多く、様々な場所での変更が予想される場合

📚今回使ってみたライブラリ

聞いたことがあったりフロントメンバーに聞いてみたりしたものたちなので特にここの選定他意はない。
Context APIは正式にはReactのビルトイン機能でありライブラリではないが、比較としてここに登場。
Reduxは 公式 でRedux Toolkitの使用が推奨されていたのでRedux Toolkitだけノミネート。

  • Context API
  • Jotai
  • MobX
  • Recoil
  • Redux Toolkit
  • XState
  • Zustand

📛やること

一律以下の流れをやってみる。

ライブラリの設定
↓
pages/user/form.tsx のフォームに入力
「更新ボタン」押下で更新
↓
「確認ボタン」押下で pages/user/profile.tsx へ遷移
入力した値が表示されているか確認する

🌵Context API

概要

Reactのビルトイン機能で、コンポーネント間でのデータの伝達を容易にする。

ビルトイン機能

Reactに元々入っている機能
公式サイト↓
https://ja.react.dev/learn/passing-data-deeply-with-context

使い方

1️⃣ Contextの作成

contexts/UserContext.tsx
import { createContext } from "react";

const UserContext = createContext({
  user: { name: "", age: 0 },
  setUser: () => {},
});

export default UserContext;

2️⃣ Providerを設置しContextを提供

pages/_app.tsx
import type { AppProps } from "next/app";
import Layout from "@/Layouts/Layout";
import UserContext from "@/contexts/UserContext";

export default function App({ Component, pageProps }: AppProps) {
  const [user, setUser] = useState({ name: "", age: 0 });

  return (
    <Layout>
      <UserContext.Provider value={{ user, setUser }}>
        <Component {...pageProps} />
      </UserContext.Provider>
    </Layout>
  );
}

3️⃣ 該当ページで使用

pages/user/form.tsx
import UserContext from "@/contexts/UserContext";
import { useRouter } from "next/router";
import { useContext } from "react";

const UserForm = () => {
 const { setUser } = useContext(UserContext);
 const router = useRouter();

 const setValues = () => {
   const name = document.querySelector('input[name="name"]').value;
   const age = document.querySelector('input[name="age"]').value;
   setUser({ name, age });
 };

 return (
   <>
     name: <input name="name" />
     age: <input name="age" />
     <button onClick={setValues}>更新ボタン</button>
     <button onClick={() => router.push("/user/profile")}>確認ボタン</button>
   </>
 );
};

export default UserForm;
pages/user/profile.tsx
import UserContext from "@/contexts/UserContext";
import { useContext } from "react";

const UserProfile = () => {
  const { user } = useContext(UserContext);

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

export default UserProfile;

特徴

  • ストアベース
  • Reactのビルトイン機能
    • ライブラリの追加が不要
    • Hooksなどと組み合わせて柔軟に使える
    • ドキュメントが豊富
  • 大規模で複雑な情報管理が必要でない場合はちょうどいい
    • ストアベースだから元々大規模開発に向いている
    • 複雑な管理などをしないのでパフォーマンスの使いすぎにならない

🎃Jotai

概要

原子(Atom)という概念を中心に構築されており、グローバルステートやローカルステートの管理をシンプルかつ効率的に行うことができる状態管理ライブラリ。
公式サイト↓
https://jotai.org/

使い方

1️⃣ Jotai のインストール

npm install jotai

2️⃣ Jotai設定ファイルの作成

./atoms/UserAtom.ts
import { atom } from "jotai";

export const userAtom = atom({ user: { name: "", age: 0 } });

3️⃣ 該当ページで使用

./pages/user/form.tsx
import { useRouter } from "next/router";
import { userAtom } from "@/stores/UserStore";
import { useAtom } from "jotai";

const UserForm = () => {
  const [, setUser] = useAtom(userAtom);
  const router = useRouter();

  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = document.querySelector('input[name="age"]').value;
    setUser({ user: { name, age } });
  };

  return (
    <>
      name: <input name="name" />
      age: <input name="age" />
      <button onClick={setValues}>更新ボタン</button>
      <button type="button" onClick={() => router.push("/user/profile")}>
        確認ボタン
      </button>
    </>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { userAtom } from "@/stores/UserStore";
import { useAtom } from "jotai";

const UserProfile = () => {
  const [{ user }] = useAtom(userAtom);

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

export default UserProfile;

特徴

  • アトムベース
  • 全体的にコード量が少ないので導入しやすい
  • 日本語のファンサイト( https://jotaifriends.dev/ )もあり日本語のナレッジが豊富
  • TypeScriptの対応が優秀
  • 自由に柔軟に使えてしまうのでコーディングルールなど規則ガチガチのプロジェクトには向いてないかも

🥭MobX

概要

効率的な状態管理ライブラリで、リアクティブなプログラミングモデルを提供。
公式サイト↓
https://mobx.js.org/README.html

使い方

1️⃣ MobXのインストール

npm install mobx mobx-react

2️⃣ MobX設定ファイルの作成

./stores/UserStore.ts
import { makeAutoObservable } from "mobx";
import { createContext } from "react";

class UserStore {
 user = { name: "", age: 0 };

 constructor() {
   makeAutoObservable(this);
 }
 setUser({ name, age }) {
   this.user.name = name;
   this.user.age = age;
 }
}

export const userStore = new UserStore();
export const StoreContext = createContext(userStore);

3️⃣ MobXの設定をアプリケーション全体に適用

./pages/_app.tsx
import { Layout } from "@/Layouts";
import { StoreContext, userStore } from "@/stores/UserStore";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <StoreContext.Provider value={userStore}>
        <Component {...pageProps} />
      </StoreContext.Provider>
    </Layout>
  );
}

4️⃣ 該当ページで使用

./pages/user/form.tsx
import { useRouter } from "next/router";
import { useContext } from "react";
import { StoreContext } from "@/stores/UserStore";

const UserForm = () => {
  const userStore = useContext(StoreContext);
  const router = useRouter();

  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = document.querySelector('input[name="age"]').value;
    userStore.setUser({ name, age });
  };

  return (
    <div style={{ paddingTop: "100px" }}>
      name: <input name="name" />
      age: <input name="age" />
      <button onClick={setValues}>更新ボタン</button>
      <button type="button" onClick={() => router.push("/user/profile")}>
        確認ボタン
      </button>
    </div>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { StoreContext } from "@/stores/UserStore";
import { useContext } from "react";

const UserProfile = () => {
  const user = useContext(StoreContext).user;

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

export default UserProfile;

特徴

  • ストアベース
  • Context APIに似ているが、よりリアクティブなデータに有用
    ※ そもそもの目的がMobX→リクティブな状態管理、ContextAPI→propsを削減したデータ共有
  • リアクティブに動けるので大規模でパフォーマンス向上が要求されるプロジェクトに向いていそう

🐰Recoil

概要

React用の状態管理ライブラリで、より効率的なコンポーネント間の状態共有を可能にする。
公式サイト↓
https://recoiljs.org/

使い方

1️⃣ Recoilのインストール

npm install recoil

2️⃣ Recoil設定ファイルの作成
Recoilでは状態を保持するものを「アトム」というらしい

./atoms/UserAtom.ts
import { atom } from "recoil";

export const userState = atom({
 key: "userState",
 default: { name: "", age: 0 },
});

3️⃣ Recoilの設定をアプリケーション全体に適用

./pages/_app.tsx
import { Layout } from "@/Layouts";
import type { AppProps } from "next/app";
import { RecoilRoot } from "recoil";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <RecoilRoot>
        <Component {...pageProps} />
      </RecoilRoot>
    </Layout>
  );
}

4️⃣ 該当ページで使用

./pages/user/form.tsx
import { useRouter } from "next/router";
import { userState } from "@/atoms/UserAtom";
import { useRecoilState } from "recoil";

const UserForm = () => {
  const [, setUser] = useRecoilState(userState);
  const router = useRouter();

  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = document.querySelector('input[name="age"]').value;
    setUser(() => ({ name, age }));
  };

  return (
    <>
      name: <input name="name" />
      age: <input name="age" />
      <button onClick={setValues}>更新ボタン</button>
      <button type="button" onClick={() => router.push("/user/profile")}>
        確認ボタン
      </button>
    </>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { userState } from "@/atoms/UserAtom";
import { useRecoilValue } from "recoil";

const UserProfile = () => {
  const user = useRecoilValue(userState);

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

export default UserProfile;

特徴

  • アトムベース
  • アトムベースの割にselector(派生状態)を用いて複雑な処理や大規模開発にも向いている
./atoms/UserAtom.ts
import { atom, selector } from "recoil";

export const userState = atom({
  key: "userState",
  default: { name: "", age: 0 },
});

export const doubleUserAgeState = selector({
  key: "doubleUserAgeState",
  get: ({ get }) => get(userState).age * 2,
});
  • 未だベータ版(0.7.7)しか存在しなく、最終更新が1年前なのであまりメンテナンスされないかも

🦖Redux Toolkit

概要

Reduxの複雑さを減らしたライブラリ。

公式サイト↓
https://redux-toolkit.js.org/

使い方

1️⃣ Redux Toolkit のインストール

npm install react-redux @reduxjs/toolkit

2️⃣ Sliceファイルの作成

./stores/UserSlice.ts
import { createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'user',
  initialState: {
    user: { name: '', age: 0 }
  },
  reducers: {
    updateUser: (state, action) => {
      state.name = action.payload.name;
      state.age = action.payload.age;
    }
  }
})

export const { setUser } = userSlice.actions;
export default userSlice.reducer;

3️⃣ Redux Toolkit設定ファイルの作成

./stores/UserStore.ts
import { configureStore } from "@reduxjs/toolkit";
import userReducer from '@/stores/userSlice';

export const store = configureStore({ reducer: { user: userReducer } });

4️⃣ Redux Toolkitの設定をアプリケーション全体に適用

./pages/_app.tsx
import type { AppProps } from "next/app";
import Layout from "@/Layouts/Layout";
import { store } from "@/stores/UserStore";
import { Provider } from "react-redux";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Provider store={store}>
        <Component {...pageProps} />
      </Provider>
    </Layout>
  );
}

5️⃣ 該当ページで使用

./pages/user/form.tsx
import { updateUser } from "@/stores/userSlice";
import router from "next/router";
import { useDispatch } from "react-redux";

const UserForm = () => {
  const dispatch = useDispatch();

  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = Number(document.querySelector('input[name="age"]').value);
    dispatch(updateUser({ name, age }));
  };

  return (
    <>
      name: <input name="name" />
      age: <input name="age" />
      <button onClick={setValues}>更新ボタン</button>
      <button type="button" onClick={() => router.push("/user/profile")}>
        確認ボタン
      </button>
    </div>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { useSelector } from "react-redux";

const UserProfile = () => {
  const user = useSelector((state) => state.user);

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

export default UserProfile;

特徴

  • ストアベース
  • Reduxから推奨されている
    • Reduxの機能そのままに、よりコード量が少ない&直感的に分かりやすい
    • ドキュメントが豊富
  • ルールが厳格なプロジェクトで散らばることなく管理できる
  • 小規模だと機能が盛りだくさんすぎるかも

🩻XState

概要

XStateは、アプリケーションの状態管理をより構造化された、予測可能な方法で行うための強力なライブラリ。
ストアベース・アトムベースという単位に包括されない。

状態マシンを使って、アプリケーションの状態とその変化を明確に定義し、管理します。

公式サイト↓
https://stately.ai/docs
※ バージョン4系はこちら https://xstate.js.org/docs/

使い方

1️⃣ XState のインストール

npm install xstate @xstate/react

2️⃣ XState設定ファイル(machine)の作成

./machines/useMachine.ts
import { createMachine, assign } from "xstate";

export const userMachine = createMachine({
 id: "user",
 initial: "idle",
 context: { user: { name: "", age: 0 } },
 states: {
   idle: {
     on: {
       update: {
         actions: assign({
           user: ({ event }) => ({ name: event.name, age: event.age }),
         }),
       },
     },
   },
 },
});

3️⃣ XStateの設定をアプリケーション全体に適用

./pages/_app.tsx
export const actor = createActor(userMachine);
actor.start();

export default function App({ Component, pageProps }: AppProps) {
  return (
    // 中身は変更ないので割愛
  );
}

4️⃣ 該当ページで使用

./pages/user/form.tsx
import { actor } from "@/pages/_app";
import router from "next/router";

const UserForm = () => {
  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = Number(document.querySelector('input[name="age"]').value);
    actor.send({ type: "update", name, age });
  };

  return (
    <>
        name: <input name="name" />
        age: <input name="age" />
        <button onClick={setValues}>更新ボタン</button>
        <button type="button" onClick={() => router.push("/user/profile")}>
          確認ボタン
        </button>
    </>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { actor } from "@/pages/_app";
import { useSelector } from "@xstate/react";

const UserProfile = () => {
  const user = useSelector(actor, (snapshot) => snapshot.context.user);

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

export default UserProfile;

特徴

  • アトムベースでもストアベースでもない
    • 「状態マシン」と呼ばれる構造を用いた特別な方法で管理
  • 状態可視化ツール( https://stately.ai/ )が備わっている
  • Reactじゃないと使えないわけじゃないので他のフレームワークを使用する開発者へのナレッジがたまる
  • 学習コストが高い

🦓Zustand

概要

他に比べ簡単に扱うことができるシンプルで軽量な状態管理ライブラリ。
公式サイト↓
https://docs.pmnd.rs/zustand/getting-started/introduction

使い方

1️⃣ Zustandのインストール

npm install zustand

2️⃣ Zustand設定ファイルの作成

./stores/UserStore.ts
import { create } from "zustand";

export const useUserStore = create((set) => ({
 user: { name: "", age: 0 },
 setUser: ({ name, age }) => set(() => ({ user: { name, age } })),
}));

3️⃣ 該当ページで使用

./pages/user/form.tsx
import { useRouter } from "next/router";
import { useUserStore } from "@/stores/UserStore";

const UserForm = () => {
  const setUser = useUserStore((state) => state.setUser);
  const router = useRouter();

  const setValues = () => {
    const name = document.querySelector('input[name="name"]').value;
    const age = document.querySelector('input[name="age"]').value;
    setUser({ name, age });
  };

  return (
    <>
      name: <input name="name" />
      age: <input name="age" />
      <button onClick={setValues}>更新ボタン</button>
      <button type="button" onClick={() => router.push("/user/profile")}>
        確認ボタン
      </button>
    </>
  );
};

export default UserForm;
./pages/user/profile.tsx
import { useUserStore } from "@/stores/UserStore";

const UserProfile = () => {
  const user = useUserStore((state) => state.user);

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

export default UserProfile;

特徴

  • アトムベース&ストアベース
    • サイトによって「アトムベース」とも「ストアベース」とも言われている
    • storeを複数作成できるので二者のいいとこ取りって感じ
  • 全体的にコード量が少ない
  • 大規模で色々なところで情報更新が行われるプロジェクトだと散ってしまうので探すのに時間がかかるかも

🪂比較

1つにまとめると見づらいのでストア部門とアトム(+その他) 部門に分けて記載

【ストアベース部門】

Context API MobX Redux Toolkit
特徴 ✅ビルトイン機能

⭐️シンプルで大規模すぎない開発向き
⭐️リアクティブなデータ向き ✅Reduxが埋め込まれている

⭐️ルールが厳格なプロジェクトに向き

【アトムベース・その他部門】

Jotai Recoil XState Zustand
特徴 ✅コード量が少ない
✅導入楽
✅🚫柔軟性が高く自由に操作できちゃう
✅派生状態で複雑な処理も対応

🚫ベータ版
✅React以外も使える
✅🚫独自の管理方法

🚫日本語のドキュメント少なめ
✅アトム&ストアいいとこ取り

🚫多くの場所で更新すると情報が分散
Arsaga Developers Blog

Discussion