📖

React 用状態管理ライブラリ「Jotai」に入門

2021/05/16に公開
2

こんにちは.株式会社ゆめみの Keeth こと桑原です.

今回は Jotai という React 用の超軽量な状態管理ライブラリを触ってみたので勉強ログとしてまとめました.軽く使ってみた感触としては非常にシンプルで分かりやすく,導入も簡単でしたのでしたね.

ただ,既に Jotai リリース後ある程度時間が経っており,Google で検索していただくとわかるかと思いますが,Jotai に関する記事もいくつかありますので,二番煎じな内容もありますことをご了承頂ければと思います.

※一応自分が手を動かしたコードのリポジトリも載せておきます 👇

https://github.com/kkeeth/my-jotai-demo

TL;DR

  • とにかく軽量で簡単
  • Redux が冗長に感じ,もっとライトなものを探している人にオススメ
  • 軽量な分ルールが少ないのでカオスにならないように注意

Jotai の概要

触ってみると共感頂けるかと思いますが,まず感じたことは React の Context APIRecoil に非常に似ています.

ちなみに発音ですが,公式リポジトリにも以下のように記載がある通り,

Jotai is pronounced "joe-tie" and means "state" in Japanese.

日本語の「状態」から名前と発音を取っていることがわかります 😂

サイズ比較

私がミニマルなライブラリが好きなので,React 用状態管理ライブラリのサイズ比較をしてみたいと思います.対象は Redux, Mobx, Recoil の名前を聞くようになったので,これらと Jotai のサイズを比べてみます.※いずれも minified + gzipped の数字です.

name size
redux 1.6kb
react-redux 5kb
mobx 16.1kB
recoil 19.7kb
jotai 2.5kb

こう見ると,Jotai は公式が謳っている通りかなり軽量であることがわかりますが,Redux 単体では更に軽量なのが意外でした.逆に Recoil は思った以上にサイズが有ることがわかりますね.

余談ですが,ダウンロード数比較をすると,やはり Redux が強いですね.ついで Mobx が頑張っている印象で,これはリリースが早かったというのも加味すると納得です.

https://www.npmtrends.com/jotai-vs-recoil-vs-mobx-vs-redux-vs-react-redux

また,公式ドキュメントのコンセプトを読んだところ,Jotai が生まれた元々の理由というか考えは,React の余分な再レンダリングの問題を解決するためだったそうです.

開発者である Daishi Kato 氏がこのように述べているように,

Jotai can be seen as a replacement for useState+useContext. Instead of creating multiple contexts, atoms share one big context.

useStateuseContext の代わりとしての面もあるとのことです.

Jotai の使い方

根本の考え方として,Jotai はローカルの React コンポーネントの state の値を atoms で置き換える.これを useAtom という hook に渡します.useAtom hook を使うことで atom の値を取得・更新をします.

では,具体的に見ていきましょう.

プリミティブな atom を生成

まずは atom と呼ばれる最小単にの状態を保持したオブジェクトを生成し,管理します.このオブジェクトを生成するには,以下のように atom というファクトリ関数に初期値を渡して実行することで可能です.

import { atom } from 'jotai'

const count = atom(0)

atom はその性質を考えるとコンポーネントで定義するのではなく,以下のように別途 Atoms.js のようなファイルに集約して一元管理し,コンポーネント毎に使いたい atom をインポートできるようにすることが望ましいですね.

Atoms.js

import { atom } from 'jotai'

export const countAtom = atom(0)
export const nameAtom = atom('keeth')

Hoge.js

import { countAtom, nameAtom } from './Atoms'

const Hoge = () => {
  return (/**/)
}
export default Hoge

The atom value is stored in a Provider state.

とあるように,atom の値は内部的には Provider(useContext の Provider のラッパー) 内で使われている useState で保持されるようです.

さらに,atom メソッドの定義(TypeScript で書かれています)を見ますと,独自にセッター/ゲッターを定義できるようで,汎用的な実装になっていますね.これはありがたい.

// primitive atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>

// read-only atom
function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>

// writable derived atom
function atom<Value, Update>(
  read: (get: Getter) => Value | Promise<Value>,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>,
): WritableAtom<Value, Update>

// write-only derived atom
function atom<Value, Update>(
  read: Value,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>,
): WritableAtom<Value, Update>

useAtomatom を利用

Atoms.js で定義して Hoge コンポーネントで読み込んだので,実際のコンポーネントで利用するには,useAtom という関数の引数にわたす必要があります.

Hoge.js

import { Provider, useAtom } from 'jotai'
import { countAtom } from './Atoms'

const Hoge = () => {
  // このコンポーネントでは countAtom を利用する
  // 合わせてセッターも返される
  const [count, setCount] = useAtom(countAtom)

  const handlePlus = () => setCount((value) => value + 1)
  const handleMinus = () => setCount((value) => value - 1)

  return (
    <>
      <h1>{count} </h1>
      <div className="buttons">
        <button onClick={handlePlus}>one up</button>
        <button onClick={handleMinus}>one down</button>
      </div>
    </>
  )
}
export default Hoge

うん,分かりやすい.というか既視感があります(笑)この useAtom の使い方は useStateuseContext を合わせたように見えますね.

computed - 計算済みの値

既存の atom に何かしらの計算処理をした新しい atom を生成するための get というメソッドが用意されています.例を見てみましょう.

Atoms.js

import { atom } from 'jotai'

export const countAtom = atom(0)
export const nameAtom = atom('keeth')
// 大文字変換した名前用の atom
export const upperNameAtom = atom((get) => get(nameAtom).toUpperCase())

利用の仕方も同様で,使いたいコンポーネント側で useAtom の引数に渡してください.

「プリミティブな atom を生成」のところで転載した atom の定義を見ますと,Promise<Value> があるように,非同期処理をすることも考慮されているようですね!公式 Demo アプリでも使われていたのでコードを一部抜粋します.(デモのリンクは本記事末に記載しています)

const postData = atom(async (get) => {
  const id = get(postId)
  const response = await fetch(
    `https://hacker-news.firebaseio.com/v0/item/${id}.json`,
  )
  return await response.json()
})

Provider で初期値を与える

Redux の Provider と同様に,Jotai にも Provider があり,その下のコンポーネント全体でアクセスできる atom を設定することができます.まずは定義から.

const Provider: React.FC<{
  initialValues?: Iterable<readonly [AnyAtom, unknown]>
  scope?: Scope
}>

initialValues というアトリビュートで設定すれば良さそうですね.Iterable な値にすることに注意.

const countAtom = atom(0)

const Root = () => (
  <Provider initialValues={[countAtom, 1]]}>
    <Component />
  </Provider>
)

その他のライブラリとの違い

サイズ比較で名前を上げた2つ(react-redux は redux と同じなので無視)との違いを軽く書き残します.

※ここはまだ入門したばかりなので,今後更新したいと思います!(するとは限らない)

Redux との違い

Redux にあった以下の4つの機能・仕様が Jotai にはありません.

  • Actions がない
  • Reducers がない
  • Dispatchers がない
  • Stores がない

それにより,ライブラリ本体のサイズも小さくなりますし,開発するときのコードの記述量も減ります.その分,ルールがないとみんなが思い思いの atom を作ったり,useState で良いところを atom を生成してしまったりなど,カオスになる可能性もあるかなと思います 🤔

Recoil との違い

公式リポジトリの README に以下のように記載がありました.

How does Jotai differ from Recoil?
・Minimalistic API
・No string keys
・TypeScript oriented

驚いたのは,Recoil のソースを軽く見たところ,確かに TypeScript で書かれていないですね.package.json には typescript の記述があり,かつ test 実行時に使っているようですが,なぜ開発時に使っていないのかは謎です.

終わりに

初期リリースが2020 年 8 月 30 日ですが,約一年後の今更知った情弱な自分です…😅

また公式サイトに行くと,色んなレシピもあり,他の状態管理ライブラリとの共存のサンプルコードもあったりと学びがあるので,本格的に導入して開発をしたい方はぜひドキュメントを見に行ってみてください.

とりま,自分は今後 React/Next で開発する際に状態管理ライブラリを入れたい場合は,Jotai は真面目に検討材料に入れていきたいなと思いました.参考になれば幸いです.

ではでは.

参考

Discussion

azuazu

驚いたのは,Recoil のソースを軽く見たところ,確かに TypeScript で書かれていないですね.package.json には typescript の記述があり,かつ test 実行時に使っているようですが,なぜ開発時に使っていないのかは謎です.

RecoilはReactと同じくFlowで書かれていて、typescriptの依存はTypeScriptの型定義ファイル(d.ts)のテスト用ですね。