KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

Jotaiの採用で可読性と保守性向上!:React.ContextからJotaiへの移行

こんにちは!KENTEMでフロントエンドエンジニアをしているS.W.です。

私が担当しているプロダクトでは、React Contextを用いた状態管理において、ひとつのContextに複数の状態変数が密集している状態でした。

その結果、

  • 可読性の低下
  • 保守性の悪化
  • パフォーマンスへの影響

といった課題が顕在化していました。

こうした問題を解決するために、まずは状態管理のアプローチを見直す必要があると考え、第一歩として状態管理ライブラリの導入を検討しました。 その結果、Jotaiを採用する決断をしました。

この記事では、Jotaiへの移行ポイントや実際の移行手順を、具体的なコード例を交えながら詳しく解説します。

よりシンプルで効率的な状態管理に興味がある方は、ぜひ参考にしてください。

Jotaiに決定したポイント

シンプルさと軽量性

Jotaiは、最小限のAPIでReactのHooksとシームレスに連携できるため、コードのシンプルさを保ちながら効率的に状態管理を行えます。

その特徴のおかげで、コードサイズもコンパクトに収まるのも大きな魅力です。

高いパフォーマンス

原子的な状態管理により、必要な部分だけを効率的に更新できるため、不必要な再レンダリングを抑制します。

これにより、アプリのパフォーマンスも向上します。

学習コストの低さ

従来の状態管理ライブラリと比べて、学習コストが非常に低く、新しい概念や複雑な仕組みを覚える必要がありません。

そのため、導入もスムーズに進み、チーム内での共有や理解が容易になります。これにより、早期に生産性の向上を実現できます。

移行の準備

現在のContext利用箇所の抽出

スムーズな移行を実現するための第一歩は、今どこでContextが使われているのかを詳細に把握することです。

Contextの洗い出しと整理

まず、現状で使われているContextの箇所を洗い出し、どの変数がどこで参照・更新されているのかを明確にします。

次に、それらの情報をカテゴリ別にエクセルや表に整理し、色分けを行います。

影響範囲の可視化とリスク管理

こうすることで、影響範囲や規模が一目でわかるようになり、計画的に移行を進めるための指針となります。

また、一気に全ての箇所を変更するのではなく、カテゴリごとに段階的に進めることで、影響範囲を限定し、リスクを最小限に抑えることも重要です。

少しずつ確実に移行を進めていくことが、安全かつ効率的な方法です。

atom移行可能な状態の判断

今回の移行対象となったContextは、基本型の変数やネストのないシンプルなオブジェクトだけで構成されていました。 このような構造の状態は、atomへの移行に非常に適しています。

Jotaiのatomとは?

Jotaiのatomは、アプリケーションの状態を表す最小単位です。 簡単に言えば、「何かの値や状態そのもの」を管理するための箱のようなものです。

実際の導入・移行ステップ

atomの管理位置決定

まず、atomの配置場所を決めるルールを設定します。 これにより、管理の煩雑さを軽減し、全体の構造を整理しやすくします。

全体に関わるatomの配置場所

システム全体に関わるatomは、atomsディレクトリの直下に配置します。 これにより、共通で使われるatomを一箇所にまとめて管理できます。

機能やコンポーネントに密接に関わるatomの配置場所

また、特定の機能やコンポーネントに密接に関連しているatomは、atomsディレクトリ内に専用のフォルダを設置し、その中に配置します。 これにより、関連性が明確になり、必要なatomを素早く見つけやすくなります。

定義ルールと管理の一貫性

さらに、原則として1つのファイルに1つのatom定義を行うルールも設けます。

このルールを守ることで、どのatomがどの機能やコンポーネントに属しているのかを一目で把握でき、作業効率やメンテナンス性も向上します。

react-app/
 ├─ src/
 │  ├─ assets/
 │  ├─ common/
 │  │  └─ atoms/
 │  │      ├─ function1/
 │  │      │  └─ SelectAtom.js      ← function1用のatom
 │  │      ├─ CounterAtom.js  ←システム全体に関わるatom
 │  │      └─ ThemeAtom.js
 │  ├─ components/

Jotai インストール

以下のコマンドを実行してJotaiのライブラリをプロジェクトに追加します。

npm install jotai

atomの定義方法

通常のatom定義

import { atom } from 'jotai';

export const CountAtom = atom<number>(0);

リセット機能付きatomの定義

import { atomWithReset } from 'jotai/utils';

const defaultActiveHoge: HogeInfo = {
  id: null,
  name: '(ほげ~)',
};

export const ActiveHoge = atomWithReset<HogeInfo>(defaultActiveHoge);

必要に応じたread/write専用atomの作成

読み込み専用(例:値の2倍を取得)

import { atom } from 'jotai';

export const readDoubleCountAtom = atom<number>((get) => get(CountAtom) * 2);

書き込み専用(例:インクリメント)

import { atom } from 'jotai';

export const incrementCountAtom = atom<null, [], void>(
  null,
  (get, set) => {
    set(CountAtom, get(CountAtom) + 1);
  }
);

コンポーネントからの操作

状態の読み取りと書き込みを行うコンポーネントではuseAtom

import { useAtom } from 'jotai';
import { CountAtom } from '../../common/atoms/CountAtom';

const MyComponent = () => {
  const [count, setCount] = useAtom(CountAtom);

  return (
    <div>
      <p>カウント:{count}</p>
      <button onClick={() => setCount(count + 1)}>インクリメント</button>
    </div>
  );
};

状態の値だけが必要なコンポーネントではuseAtomValue

書き込み専用の変数として使うことで、コンポーネントの責務が明確になります。

import { useAtomValue } from 'jotai';
import { CountAtom } from '../../common/atoms/CountAtom';

const MyComponent = () => {
  const count = useAtomValue(CountAtom);

  return (
    <div>
      <p>現在のカウント:{count}</p>
    </div>
  );
};

状態の値を読む必要がなく、変更だけ行うコンポーネントではuseSetAtom

値の読み取りが不要なので、値の変化による再レンダリングが抑えられます。

import { useSetAtom } from 'jotai';
import { CountAtom } from '../../common/atoms/CountAtom';

const MyComponent = () => {
  const setCount = useSetAtom(CountAtom);

  const increment = () => {
    setCount(prev => prev + 1);
  };

  return (
    <button onClick={increment}>カウントを増やす</button>
  );
};

useResetAtomでリセット機能付きatomをリセット

import { useAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { CountAtom } from '../../common/atoms/CountAtom';

const MyComponent = () => {
  const [count, setCount] = useAtom(CountAtom);
  const resetCount = useResetAtom(CountAtom);

  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);

  return (
    <div>
      <h2>カウンター</h2>
      <p>現在の値: {count}</p>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
      <button onClick={resetCount}>リセット</button>
    </div>
  );
};

既存のContext内の段階的なAtom移行と最終的なシンプル化

Context内の状態管理を段階的にatomへ置換します。

最終的にContextが不要となったタイミングでProviderを削減し、直接atomを利用したシンプルな構成に移行します。

// Context Provider
import React, { useState, createContext, useContext } from 'react';
import HogeContext from './HogeContext'; // 既存のコンテキスト

const Index = () => {
  const [isHoge, setHoge] = useState(false);
  const [Page, setPage] = useState(PageKind);

  return (
    <div>
      <HogeContext.Provider value={{ isHoge, setHoge, Page, setPage }}>
        <MyComponent  />
      </HogeContext.Provider>
    <div/>
  );
};

const MyComponent = () => {
  const hogeContext = useContext(HogeContext);

  return (
    <div>
      {/* コンテキストを複数の子コンポーネントに */}
      <HogeContext.Provider value={hogeContext}>
        <MyComponent1 />
      </HogeContext.Provider>
      
      <HogeContext.Provider value={hogeContext}>
        <MyComponent2 />
      </HogeContext.Provider>
    </div>
  );
};
// Context Provider
import React, { useState, createContext, useContext } from 'react';

const Index = () => {

  return (
    <div>
      <MyComponent  />
    <div/>
  );
};

const MyComponent = () => {
  const hogeContext = useContext(HogeContext);

  return (
    <div>
      {/* atomを直接使うことにより、Context Providerを排除 */}
      <MyComponent1 />
      <MyComponent2 />
    </div>
  );
};

困ったこと

atomの値を更新した際に、更新対象のatomを依存配列に含めている他の画面の子コンポーネントでuseEffectやuseMemoが再実行されてしまうという現象に直面しました。

React Contextを利用していた場合は、Providerで該当のコンポーネントをネストしていたため、この現象は発生しませんでした。

対応策

この問題に対して最も影響を少なくできると考えたのが、useStateを使う方法です。

useStateは、そのコンポーネント内部だけの状態を管理できるため、他のコンポーネントに副作用をもたらす心配がほとんどありません。

特に、マウント時に参照しているだけで、該当のコンポーネント内でatomの更新を行わない場合には有効です。

// 依存配列に含めるコンポーネントではuseStateを利用し、その初期値にatomを設定する
const activeConstructionAtom = useAtomValue(ActiveConstructionAtom);
const [activeConstruction] = useState(activeConstructionAtom);

まとめと今後の展望

Jotaiを導入したことで、グローバルな状態の追加や管理が格段にやりやすくなったと実感しています。 特に、シンプルなAPIのおかげで、状態の扱いが直感的になり、開発効率も向上しました。

また、Redux DevToolsを利用することにより、状態の変化を視覚的に追跡できるため、デバッグがより効率的になりました。

現状は、ProviderやStoreを使わずに、グローバルな状態管理を行っています。これらの仕組みを取り入れると、状態を切り分けた空間ごとに独立した管理が可能になる反面、見通しの悪化やコンポーネントの独立性が下がる恐れもあります。 テスト面ではこれらの仕組みを活用できるとの情報もあり、今後はテストや拡張性の観点から導入を検討したいところです。

また、現在使っている特定の機能だけに限定されたContextには、React.MutableRefObjectが含まれていて、その動作が不安定になる可能性があります。そこで、現状ではJotaiへの完全な移行は一旦見送りました。 ただし、複雑なオブジェクトや入れ子になった構造についても、要素ごとにatomに分解して段階的に置き換えることができるので、今後の計画として検討を進めていきたいと思っています。

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp