✏️

【React】Jotai を使ってみて

2024/01/08に公開

はじめに

今までReactでのコンポーネント間におけるstate(グローバルステートの)管理ではContextばかり使ってきたのですが、幅を広げたいと思ってJotaiを使ってみました。今回は、Contextばかり使ってきた筆者の所感と、その比較を交えて書いていこうと思います。

まず結論

Jotaiすごく使いやすかったです。今後はContextではなく、Jotaiをメインに使っていこうかなと思いました(実際は、その時の状況や仕様に応じてフレキシブルに選択すると思いますが)。

新しい技術に慣れるにはインプットからのアウトプットが常套手段なので、今回まずは以下の記事とチュートリアルを元に試用。

参考にさせていただいた記事:
https://qiita.com/moritakusan/items/9a5e8c315b2565a02848

そのあと応用編として自分で使用してみました。
応用編で作ったものは以下になります(PokeAPIを利用しています)。

Jotai に好感を持てた点

  • useAtomuseStateuseContextを足した感じで、シンプルで分かりやすくグローバルステートを(更新・管理など)扱える
  • Providerを使うことも、使わないこともできる
  • 前もってatomstate)の更新処理を仕込んでおける
  • atomを外部ファイル(例:atom.ts)に分割して一元管理できるので楽

ざっと挙げてみてこのくらいで、今後使っていくにつれて増えていくかもしれません。

  • useAtomuseStateuseContextを足した感じで、シンプルで分かりやすくグローバルステートを(更新・管理など)扱える
    まずはuseStateのような使用感について下記コードをもとに説明します。
import { atom, useAtom } from "jotai"
..
.
// atom に初期値100を指定
const valueAtom = atom(100)
.
..
export const TheComponent = () => {
..
.
// valueAtom(初期値100を指定したatom)を state に変数と更新関数を用意
const [value, setVal] = useAtom(valueAtom) 
// useState と同じ要領で使用(変数の更新)
const setValue = (x: number) => setVal((_prevVal) => value * x);
..
.
<button type="button" onClick={() => setValue(2)}>
  <b>value:{value}</b><br />値を2倍にしていく
</button>
..
.

このようにuseStateと同じ要領で atom を更新できます。
const setValue = (x: number) => setVal((_prevVal) => value * x);

次に、useContextのような使用感について。

.
..
/* -------- 外部ファイル(ts/atom.ts)で宣言した atom を呼び出して使用する -------- */
const [pokeItem] = useAtom(pokeItemAtom);
const [pokeList] = useAtom(pokeListAtom);
..
.
/* 呼び出した atom をコンポーネントに渡す */
<SingleData pokeItem={pokeItem} />
<MultiData pokeList={pokeList} />
..
.

もちろん、コンポーネントで直接atomを呼び出してもokです。

import { useAtom } from "jotai";
import { pokeListAtom } from "./ts/atom";

export const MultiData = () => {
    const [pokeList] = useAtom(pokeListAtom); // `atom`を呼び出す

    return (
        <div style={listStyle}>
            {pokeList.map((pokemon, i) => (
                /* 処理 */
            ))}
        </div>
    );
}

ちなみに、先ほど挙げた好感ポイントの3つ目にある、

  • 前もってatomstate)の更新処理を仕込んでおける
    に関して、先ほどのuseStateの使用感についてのコードを以下のようにすることで同様の結果が得られます。
import { atom, useAtom } from "jotai"
..
.
// atom に初期値100を指定
const valueAtom = atom(100)
const addingValueAtom = atom(
    (get) => get(valueAtom), // valueAtom(初期値100を指定したatom)を取得
    (get, set, num: number) => {
        set(valueAtom, get(valueAtom) * num) // 取得した valueAtom に num を掛けて更新
    },
)
.
..
export const TheComponent = () => {
..
.
// addingValueAtom を state に変数と更新関数を用意
const [value, setValue] = useAtom(addingValueAtom) 
..
.
<button type="button" onClick={() => setValue(2)}>
  <b>value:{value}</b><br />値を2倍にしていく
</button>
..
.

この部分は先ほど紹介した参考記事から拝借しており、そちらでより詳しく説明していただいているので是非ご確認ください。

  • Providerを使うことも、使わないこともできる
    Contextでグローバルステートの管理を行うと、再レンダリング(*1)のことを考慮して設けたグローバルステート(Context)の数だけProviderを用意することになり、以下のようにProviderによって階層が深くなってしまう事態もあります。
    *1:contextの値が更新・変更されると、そのcontextを参照しているコンポーネント全てが再レンダリングされる
.
..
<React.StrictMode>
    <TheHogeContextFlagment>
      <TheFooContextFlagment>
        <TheBarContextFlagment>
          <App />
        </TheBarContextFlagment>
      </TheFooContextFlagment>
    </TheHogeContextFlagment>
</React.StrictMode>,
..
.

しかし、jotaiではこのようなことをしても良いし、しなくても良いような作りになっています。
具体的には、この好感ポイントも参考記事にて分かりやすく説明されていますが、こちらでは筆者が試用検証したもので進めていきます。

以下のコンポーネント(Counter.tsx, CountUpdateBtn.tsx)は中身が同じカウント+1するためのボタンコンポーネントで、カウントの更新にはcountsAtomというatomを使っています。

Counter.tsx
import { useAtom } from "jotai";
import { countsAtom } from "./ts/atom";

export const CountBtn = () => {
    // countsAtom を state に変数と更新関数を用意
    const [count, setCount] = useAtom(countsAtom);
    return (
        <>
            <div>Counter:{count}</div>
            <button onClick={() => setCount((p) => p + 1)}>
                +1するボタン
            </button>
        </>
    )
}
CountUpdateBtn.tsx
.
..
// button 要素にスタイルをあてているだけで、他は Counter.tsx と同じ内容
<button style={{ 'borderColor': 'red' }} onClick={() => setCount((p) => p + 1)}>
..
.

atomを管理する外部ファイル(atom.ts

atom.ts
import { atom } from "jotai";
export const countsAtom = atom(0);
TheComponent.tsx
import { Provider, atom, createStore, useAtom } from "jotai"
import { countsAtom } from "./ts/atom"
import { CountBtn } from "./CountBtn"
import { CountUpdateBtn } from "./CountUpdateBtn"

// store(共有するデータの保管場所を定義するもの)を宣言
const countsStore = createStore()
// storeの中にcountsAtomを初期値100としてセットする
countsStore.set(countsAtom, 100)
.
..
export const TheComponent = () => {
..
.
<Provider store={countsStore}>
  <h1>Proverで切り分けした空間</h1>
  <CountUpdateBtn />
  <CountUpdateBtn />
  <CountBtn />
</Provider>
<h1>グローバルステート空間</h1>
<CountBtn />
..
.

Counter.tsxCountUpdateBtn.tsxは処理は同じでも別々の異なるコンポーネントです。ですがボタンを押下するとstoreで指定した値(100)が更新されていきます。

countsStore.set(countsAtom, 100)

gif画像-1

一方、Providerに囲われていないCounter.tsx(CountBtn コンポーネント)を押下するとatom.tsで指定したcountsAtom値(0)が更新されていきます。

export const countsAtom = atom(0);
  • atomを外部ファイル(例:atom.ts)に分割して一元管理できるので楽
    上記ではatom.tsで一つのatom(countsAtom)しか管理していないのでメリットを感じづらいかもしれません。しかし管理するatomが増えてくると一元管理できるありがたみが実感できます。

以下は冒頭に掲載した応用編で作ったもののatom.tsです。

import { atom } from "jotai";
import { pokeDataType } from "./pokedata";

const defaultPokeListData: pokeDataType = {
    name: '【hogemon】---',
    weight: 100,
    height: 100
}

const defaultPokeSingleData: pokeDataType = { name: undefined }

// ポケモンの入れ替え用 atom
// 引数には defaultPokeSingleData ではなく、defaultPokeListData でもok.
export const pokeItemAtom = atom(defaultPokeSingleData);

// ポケモンの追加用 atom
export const pokeListAtom = atom([defaultPokeListData]);

ここは少し詰まったところだったのですが、atomを作成する際のデフォルト値の型はneverだそうで「atomに型注釈ってどうするのだろう?」と右往左往しました。
そこでChatGPTに質問を投げると、以下のように自身でデフォルト値を設ける必要があることを知りました。

Jotai の atom はデフォルトでは初期値の型を never として扱います。これに対処するためには、atom を作成する際にジェネリクスを使用して、初期値の型を指定することができます。

そのため上記ではdefaultPokeListData: pokeDataTypedefaultPokeSingleData: pokeDataTypeなどを用意してそれをatomの引数に指定しています。

// 最初は never だが、
const pokeItemAtom: PrimitiveAtom<never> & WithInitialValue<never>

// 引数に型注釈したものを指定すると型を解釈してくれる。すごい Jotai というか TypeScript?
const pokeItemAtom: PrimitiveAtom<pokeDataType> & WithInitialValue<pokeDataType>

デフォルト値とかを各コンポーネントごとに用意する必要が特になければ一元管理できた方が楽ですよね。

さいごに

まだまだJotaiを完全に理解し、マスターできたわけではありませんが、今後積極的に使って習熟度を高めたいと思います。
あと、言わずもがなグローバルステートの管理にはReduxRecoil, Zustandなどたくさんありますので、追々それらにも手を伸ばして幅を広げたいなと。

Contextばかり使っている筆者が言うのもなんですが、今回使ってみたJotaiは身構えて臨んだ割に拍子抜けするくらいシンプルで分かりやすい状態管理ライブラリでした!
まだ使ったことのない方がいらしたらぜひ試してみてください。

https://jotai.org/

ここまで読んでいただき、ありがとうございました。

参考記事

https://qiita.com/moritakusan/items/9a5e8c315b2565a02848

https://jotai.org/

Discussion