8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pmndrs/jotaiのドキュメントをざっと読んでみた

Last updated at Posted at 2021-02-04

core

atom生成

  • atom
  • atomFamily
  • atomFrozenInDev
  • atomWithImmer
  • atomWithReducer
  • atomWithReset
  • withImmer

atom使用

  • useAtom
  • useAtomCallback 再描画抑止
  • useAtomValue
  • useBridge
  • useImmerAtom
  • useReducerAtom
  • useResetAtom
  • useSelector 廃止 (selectAtomに変更)
  • useUpdateAtom 再描画抑止

特殊系

  • focusAtom
  • freezeAtom
  • selectAtom

atom

それ自体には状態を持たず、値はProviderに保存される。
参照するコンポーネントが全てunMountされるとProviderの値も消える。
どこで作っても良くて(コンポーネント内外問わず)、Reactのstateに入れても良い

debugLabel

React Dev Toolsを使い、Providerを見るとDebugStateという項目があり、ここにatomの値が確認できる。
debugLabelを使わないとatomの名前は連番になるのでどれがどれか分かりにくい。この為debugLabelを使ってわかりやすく出来る。

onMount

const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
  console.log('returnするまではonMountで実行される')
  setAtom(c => c + 1) // 初期値の設定などに便利
  return () => { ... } // unMount時に実行される
}

これを指定すると、そのatomが最初にProviderに保存される時に実行される。
全ての参照するコンポーネントが無くなるとunMountが実行される(return () => ...は必要がなければ返さなくていい void可)。

Provider

atomの値が保存される場所。基本コンポーネントRootに一つあれば良い。

initialValues

初期値の指定が可能。

scope

状態管理上、複数Providerを持ちたい場合はscopeを指定してやれば、それぞれのProvider対atomで管理できる。

useAtom

React.useStateに似てる使い方。

const anAtom = atom(1, (get, set, update) => {...})

...

const [value, setValue] = useAtom(anAtom)

ここで言うsetValueは1つの値だけを取り、それが(get, set, update) => {...}update部分に渡されることになる。

useBridge/Bridge

よく分からない。

utils

useUpdateAtom

import { atom, Provider, useAtom } from "jotai"
import React from "react"

const countAtom = atom(0)

interface Props {}

const CounterDisplay: React.FC<Props> = (props) => {
  console.log("CounterDisplay:")
  const [count] = useAtom(countAtom)
  return <>{count}</>
}
const Counter: React.FC<Props> = (props) => {
  console.log("Counter:")
  const [, set] = useAtom(countAtom)

  return (
    <button
      onClick={() => {
        set((prev) => prev + 1)
      }}
    >
      up
    </button>
  )
}

const index: React.FC<Props> = (props) => {
  console.log("index:")
  return (
    <Provider>
      <CounterDisplay />
      <Counter />
    </Provider>
  )
}
export default index

こんな感じで同じatomを別のコンポーネントで参照していて、一方は値のみ参照、もう一方は値の更新のみ担当みたいな感じで使っている場合

  1. ボタンを押す
  2. イベントが発生
  3. setを実行
  4. countが更新される
  5. CounterDisplayコンポーネントが再描画
  6. Counterコンポーネントも再描画(atomが更新されているので...)

6.が無駄だよねってことで

import { useUpdateAtom } from "jotai/utils"

...

const Counter: React.FC<Props> = (props) => {
  const set = useUpdateAtom(countAtom)

...

のようにアップデート関数だけを抽出しておけば、atomの値の変化にコンポーネントが反応しない=不必要な再描画がない。

多岐にわたるコンポーネントで同じatomを使用する場合、再描画を抑えるために出来るだけatomはatom系で作っておいて、useUpdateAtomを使えるところは使ったほうが良さそう。

atomWithReducer例

先にatomがあって、useReducerAtomを使うとuseUpdateAtomは併用できない

const reducer = (prev, action) => {
  if (action.type === "inc") return prev + 1
  if (action.type === "dec") return prev - 1
  throw new Error("unknown action type")
}
const counter = atomWithReducer(0, reducer)

const A: React.FC<Props> = (props) => {
  console.log("A:")
  const [count, dispatch] = useAtom(counter)
...
}

const B: React.FC<Props> = (props) => {
  console.log("B:")
  // ここで普通にこうしてしまうと、Aでdispatchされた際にこのBも再描画される
  // const [, dispatch] = useAtom(counter)
  const dispatch = useUpdateAtom(counter)
  return (
...
}

useAtomValue

useUpdateAtomはアップデート関数だけを抽出するのに対して、useAtomValueは値だけを抽出する(Recoilに合わせたAPIらしいが、再描画を抑えるためではないと思う、シンタックスシュガー的な?)。
const [value, ] = useAtom(atom)でも同じ気がする... (よく見てみたら実際にそう

atomWithReset

atom生成時とかに初期値設定してても、一旦変更した後、「あー初期値に戻したい」ってなったら結構面倒くさくない?
という事で初期値の戻せるatomがあれば良いよねという感じのatom。

useResetAtomと併用する必要がある。

useResetAtom

atomWithResetのリセット機能付きatomを、実際にリセット(初期値に戻す)時に実行するfunctionを生成するフック。

import { atomWithReset, useResetAtom } from 'jotai/utils'

const count = atomWithReset(100)
...

const reset = useResetAtom(count)

...
<button onClick={() => { reset() }}>初期値100に戻す</button>

RESET

atomWithResetで作ったatomならRESETという特殊なSymbolが使えて、これを元に独自のリセットを実装できる。

useReducerAtom

atomを使ってreducerを簡単に作れるフック。reducerで返す値がatomのsetにそのまま渡されるので、返す値=新たな値。

atomWithReducer

やってることはuseReducerAtomと同じだけど、

useReducerAtom

  • atomを作る
  • useReducerAtomにこのatomとreducerを指定する

atomWithReducer

  • atomを作ると同時にreducerも指定する

atomを作る時に最初からreducer使うことが分かっているのなら、atomWithReducerを使うかどうかの違いと思う。

atomFamily

Mapのようにatomを束ねて一纏めにしたい時に便利。(TODOリスト的な不特定多数なTODOを管理したい時とか)

引数は

  1. [必須] 保存されるatomとなるものの初期値をfunctionで指定 参照atom(defaultValue)部分と同じ)
  2. atomをどういう風にアップデートするか(atom(..., (get, set, update) => {...})部分と同じ) 参照
  3. 読み出す際にどいう言う比較して、一致する値とするか 参照
                //  1.の部分 |  2.の部分
const anAtom = atom(1,      (get, set, update) => {...})

最初に入れた値がMapのキーになる

import { atomFamily } from 'jotai/utils'

const todoFamily = atomFamily((name) => name)

todoFamily('foo') // => foo
todoFamily({id: "abc", value: 123}) // => {id: "abc", value: 123}

ドキュメントに書かれていはいないが.removeでfamily内の特定のatomを削除できる。

const aFamily = atomFamily((x) => x)

aFamily('foo')
aFamily.remove('foo')

const param = {id: "abc", value: 123}
aFamily(param)
aFamily.remove(param) // 比較する関数を指定していないので、削除する時は全く同じオブジェクトが必要(内部でMapを使っている)

const bFamily = atomFamily((obj) => obj, null, (a, b) => a.id === b.id)
bFamily({id: "abc", value: 123})
bFamily.remove({id: "abc"}) // 比較する関数を指定しているので、idだけ渡せばこの場合はOK

また、.setShouldRemoveというのもある。これは通常使う必要はなく、どうしても独自のガーベッジコレクション的なものを実装したい場合に設定する。
(基本的にfamily追加時のミリ秒を持っているので、それで比較するなど...)

useSelector 廃止(selectAtomが追加)

useSelector(anAtom, selector, equalityFn)

複数のメンバーを持つオブジェクトをatomにしてる場合、一部が更新されたら全体を参照するのではなく、一部の更新は一部のみで対応して再描画を抑えたい時に便利。

内部でuseMemoのdependenciesに使っているので、selector, equalityFnはコンポーネントの外に書ければそうしたほうが良い。そうじゃなければuseCallbackを使って逐次オブジェクトとして変わらないようにしなければ再描画が常に行われることになってしまう。
dependenciesが多くてuseCallbackとか気を使うのであれば、後から出てくるfocusAtomのほうが楽かも。

selectAtom

useSelectorと基本同じだけど、フックではなくatomに変更。

useAtomCallback

フックが使えないコンポーネント以外でatom使いたくない?
って言うことから追加されたけど、結局useフックなのでコンポーネントの中からしか使えない...のはなぜ。
多岐にわたるコンポーネントで同じatomを参照している場合、atomの値が変更されるとどうしても使用している全てのコンポーネントの再描画が起きてしまう。

このフックを使えば、値の変更を随時表示させるとか計算させる 必要がない(再描画不要) んだけど、値の参照はクリックイベントの時だけしたい、とか出来る。(useUpdateAtomの値版というか...)

これもuseMemoを使っているので、渡すcallback関数はコンポーネント再描画毎に逐次変わらないようにuseCallback使わないと無駄な再描画が起きる。

callbackをラップしたPromiseを返してくるので、async/awaitを使わないといけない

import { atom, Provider, useAtom } from "jotai"
import { useAtomCallback } from "jotai/utils"
import React from "react"

const a = atom(0)

const A: React.FC<Props> = (props) => {
  console.log("A:")
  const [count, set] = useAtom(a)
  return (
    <>
      {count}
      <button
        onClick={() => {
          set((prev) => prev + 1)
        }}
      >
        up
      </button>
    </>
  )
}
const B: React.FC<Props> = (props) => {
  console.log("B:")
  const callback = useAtomCallback(React.useCallback((get) => get(a), []))
  return (
    <>
      <button
        onClick={async () => {
          console.log("callback():", await callback())
        }}
      >
        call back
      </button>
    </>
  )
}

interface Props {}
const index: React.FC<Props> = (props) => {
  console.log("index:")
  return (
    <>
      <Provider>
        <A />
        <B />
      </Provider>
    </>
  )
}
export default index

因みにドキュメントには書かれていないが、setも出来るし、callbackにパラメータを渡すことも出来る。

const callback = useAtomCallback(
  React.useCallback((get, set, arg) => {
    console.log("set:", set) // atomをsetしようと思えば出来る
    console.log("arg:", arg) // callback(1)すれば、1が来る
    return get(a)
  }, [])
)

...

<button
  onClick={async () => {
    console.log("callback():", await callback(1))
  }}
>
  call back
</button>


freezeAtom

atomをimmutableにしてしまい、デバックしやすくするため。
atomにオブジェクトとかを入れていたとして、どこかで期待以外の値に変更されるなどが起きた場合、そのイベント事前に固まらせておいて、エラーを起こさせてデバックできるとか?

atomFrozenInDev

freezeAtomと同じだけど、NODE_ENV=developmentの時だけ固まる。

immer

使うには別途immerが必要。

yarn add immer

atomWithImmer

immerを使ったことがあるならわかるけど、複雑なオブジェクトをimmutableとして扱わずにガンガン変更して、そのままでいいので便利。

import { atomWithImmer } from "jotai/immer"

const counterAtom = atomWithImmer({ count: 0 })

const Counter: React.FC<Props> = (props) => {
  const [counter, setCounter] = useAtom(counterAtom)
  return (
    <button
      onClick={() => {
        // 通常だと新しいオブジェクトが必要
        // setCounter((counter) => {
        //   const temp = {...counter}
        //   temp.count += 1
        //   return temp
        // })
        setCounter((counter) => {
          counter.count += 1
          // オブジェクトなら値を変更して何も返さないでいい、返してもいいけど
          // プリミティブな値ならその値を返す
        })
      }}
    >
      {counter.count}
    </button>
  )
}

withImmer

atomWithImmerと基本同じだが、すでにあるatomをImmer化したatomを返す。

useImmerAtom

通常のatomをimmer化したものとして使う。
※atomWithImmerやwithImmerでimmer化したatomと併用すべきではない。

const counterAtom = atomWithImmer(0)
const regularAtom = atom(0)
const immeredRegularAtom = withImmer(regularAtom)

...

const [value, setValue] = useImmerAtom(counterAtom) // 良くない
const [value, setValue] = useImmerAtom(regularAtom) // これは良い
const [value, setValue] = useImmerAtom(immeredRegularAtom) // 良くない

optics

atomを一緒くたに使うのは簡単だけど、バラすのはどう?、と言うのから出来たoptics。

使うには別途optics-tsが必要。

yarn add optics-ts

複雑な階層を持つオブジェクトをatomとした場合、全体を変更するのではなく、パーツパーツで切り出してそれぞれを個別のatomにしてしまえば管理しやすいよね?的な。(useSelectorは参照のみだが、こちらは参照と変更ができる。)
切り出したとて大元のatomへも変更の通知は行くので便利。

focusAtom

例は単純に切り出しただけだけど、optics-tsは色々出来る(Lens、Prism、Isomorphism [用語が独特])んで要参照。

import { atom } from 'jotai'
import { focusAtom } from 'jotai/optics'

const objectAtom = atom({ a: 5, b: 10 })
const aAtom = focusAtom(objectAtom, (optic) => optic.prop('a')) // 切り出す
const bAtom = focusAtom(objectAtom, (optic) => optic.prop('b')) // 切り出す
const Controls = () => {
  // a, bそれぞれ変更しても、objectAtomも逐次変更される
  const [a, setA] = useAtom(aAtom)
  const [b, setB] = useAtom(bAtom)
  return (
    <div>
      <span>Value of a: {a}</span>
      <span>Value of b: {b}</span>
      <button onClick={() => setA((oldA) => oldA + 1)}>Increment a</button>
      <button onClick={() => setB((oldB) => oldB + 1)}>Increment b</button>
    </div>
  )
}
8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?