👻

React状態管理ライブラリ Jotai のCore API #JotaiFriends

2022/01/18に公開

こちらで紹介している公式のPrimitivesと内容が被るところがあります。このページはより丁寧に仕様が紹介されています。
筆者のコメントは少なめです。

atom

atom is a function to create an atom config. The atom config is an immutable object. The atom config itself doesn't hold an atom value. The atom value is stored in a Provider state.

atom はatom configを作成するための関数です。atom configはイミュータブルなオブジェクトです。atom config自身はatomの値を保持しません。atomの値はProviderのステートに保存されます。

// 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>
  • initialValue: as the name says, it's an initial value which is the atom's going to return unless its value doesn't get changed.
  • initialValue: 名前の通り、atomが返す初期値で、その値が変更されない限りその値を返します。
  • read: a function that's going to get called on every re-render. The signature of read is (get) => Value | Promise<Value>, and get is a function that takes an atom config and returns its value stored in Provider described below. Dependency is tracked, so if get is used for an atom at least once, the read will be reevaluated whenever the atom value is changed.
  • read: 再レンダリングの度に呼び出される関数です。read のシグネチャは (get) => Value | Promise<Value> で、get はatom config を受け取り、後述の Provider に格納された値を返す関数です。依存関係は追跡され、atomに対して get が一度でも使用された場合、atomの値が変更されるたびに read が再評価されます。
  • write: a function mostly used for mutating atom's values, for a better description; it gets called whenever we call the second value of the returned pair of useAtom, the useAtom()[1]. The default value of this function in the primitive atom will change the value of that atom. The signature of write is (get, set, update) => void | Promise<void>. get is similar to the one described above, but it doesn't track the dependency. set is a function that takes an atom config and a new value which then updates the atom value in Provider. update is an arbitrary value that we receive from the updating function returned by useAtom described below.
  • write: 主にatomの値を変更するために使用される関数で、より良い説明のために、 useAtom の戻り値のペアの2番目の値、 useAtom()[1] を呼び出すたびに呼び出されます。プリミティブatomのこの関数のデフォルト値は、そのatomの値を変更することになります。write のシグネチャは (get, set, update) => void | Promise<void> です。get は前述のものと似ていますが、依存関係を追わないのが特徴です。set はatomの設定と新しい値を受け取る関数で、Providerのatomの値を更新します。update は、後述する useAtom が返す更新関数から受け取る任意の値です。
// useAtom()[1] とは 以下でいう setState のこと
const [state, setState] = useAtom(primitiveAtom);
const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)

There are two kinds of atoms: a writable atom and a read-only atom. Primitive atoms are always writable. Derived atoms are writable if the write is specified. The write of primitive atoms is equivalent to the setState of React.useState.

atomには、書き込み可能なatomと読み取り専用のatomの2種類があります。プリミティブatomは常に書き込み可能です。派生atomは write が指定された場合、書き込み可能です。プリミティブatomの writeReact.useStatesetState と等価です。

debugLabel

The created atom config can have an optional property debugLabel. The debug label will be used to display the atom in debugging. See Debugging guide for more information.

作成されたatom configには、オプションで debugLabel というプロパティを指定することができます。デバッグラベルは、デバッグ時にatomを表示するために使用されます。詳しくはデバッグガイドを参照してください。

このように設定します。debugLabelはデバッグ用の機能です。Jotaiでは、React DevToolsとなんとRedux DevToolsが使えます。ツール連携時に役立つのがこのラベルです。

const somethingAtom = atom(...);
somethingAtom.debugLabel = 'something';

Note: Technically, the debug labels don’t have to be unique. However, it’s generally recommended to make them distinguishable.

注:技術的には、デバッグラベルはユニークである必要はありません。しかし、一般的には区別できるようにすることが推奨されます。

onMount

onMount関数内でネットワーク通信やlocalStorageとの連携など用途は様々です。

The created atom config can have an optional property onMount. onMount is a function which takes a function setAtom and returns onUnmount function optionally.

作成されたatom configは、オプションプロパティのonMount を持つことができます。onMount は関数 setAtom を受け取り、オプションで onUnmount 関数を返します。

The onMount function will be invoked when the atom is first used in a provider, and onUnmount will be invoked when it’s not used. In some edge cases, an atom can be unmounted and then mounted immediately.

プロバイダ内でatomが最初に使用されたときに onMount 関数が呼び出され、使用されなかったときに onUnmount 関数が呼び出されます。エッジケースでは、atomをアンマウントして、すぐにマウントすることもできます。

const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
  console.log('atom is mounted in provider')
  setAtom(c => c + 1) // increment count on mount
  return () => { ... } // return optional onUnmount function
}

Invoking setAtom function will invoke the atom’s write. Customizing write allows changing the behavior.

setAtom 関数を呼び出すと、atomの write が呼び出されます。write をカスタマイズすることで、振る舞いを変更することができます。

const countAtom = atom(1)
const derivedAtom = atom(
  (get) => get(countAtom),
  (get, set, action) => {
    if (action.type === 'init') {
      set(countAtom, 10)
    } else if (action.type === 'inc') {
      set(countAtom, (c) => c + 1)
    }
  }
)
derivedAtom.onMount = (setAtom) => {
  setAtom({ type: 'init' })
}

Provider

ProviderはinitialValuesとscopeを受け取ることが出来ます。こちらも初登場ですね。

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

Atom configs don't hold values. Atom values reside in separate stores. A Provider is a component that contains a store and provides atom values under the component tree. A Provider works just like React context provider. If you don't use a Provider, it works as provider-less mode with a default store. A Provider will be necessary if we need to hold different atom values for different component trees. Provider also has some capabilities described below, which doesn't exist in the provider-less mode.

Atom configは値を保持しません。Atomの値は、別のストアに存在します。Providerはストアを含むコンポーネントで、コンポーネントツリーの下にatomの値を提供します。ProviderはReactのコンテキストプロバイダと同じように動作します。Providerを使わない場合は、デフォルトのストアを持つプロバイダレスモードとして動作します。コンポーネントツリーごとに異なるatomの値を保持する必要がある場合は、Providerが必要になります。また、Providerには以下のような機能があり、provider-lessモードには存在しません。

const Root = () => (
  <Provider>
    <App />
  </Provider>
)

initialValues prop

A Provider accepts an optional prop initialValues which you can specify
some initial atom values.
The use cases of this are testing and server side rendering.

Providerはオプションで initialValues というプロパティを受け取ることができ、このプロパティでいくつかのatom初期値を指定できます。
このユースケースとしては、テストとサーバーサイドレンダリングがあります。

Example

const TestRoot = () => (
  <Provider
    initialValues={[
      [atom1, 1],
      [atom2, 'b'],
    ]}
  >
    <Component />
  </Provider>
)

TypeScript

The initialValues prop is not type friendly.
We can mitigate it by using a helper function.

initialValues propはタイプフレンドリーではありません。
ヘルパー関数を使うことで緩和することができます。

const createInitialValues = () => {
  const initialValues: (readonly [Atom<unknown>, unknown])[] = []
  const get = () => initialValues
  const set = <Value>(anAtom: Atom<Value>, value: Value) => {
    initialValues.push([anAtom, value])
  }
  return { get, set }
}

scope prop

A Provider accepts an optional prop scope which you can use for scoped Provider. When using atoms with a scope, the provider with the same scope will be used. The recommendation for the scope value is a unique symbol. The primary use case of scope is for library usage.

Provider はオプションで scope propを受け取ることができ、これはスコープされた Provider に使用できます。atomをスコープ付きで使用する場合、同じスコープを持つプロバイダが使用されます。スコープの値として推奨されるのは、ユニークなシンボルです。スコープの主な使用例は、ライブラリの使用です。

Example

const myScope = Symbol()

const anAtom = atom('')

const LibraryComponent = () => {
  const [value, setValue] = useAtom(anAtom, myScope)
  // ...
}

const LibraryRoot = ({ children }) => (
  <Provider scope={myScope}>
    {children}
  </Provider>
)

useAtom

// primitive or writable derived atom
function useAtom<Value, Update>(atom: WritableAtom<Value, Update>, scope?: Scope): [Value, SetAtom<Update>]

// read-only atom
function useAtom<Value>(atom: Atom<Value>, scope?: Scope): [Value, never]

The useAtom hook is to read an atom value stored in the Provider. It returns the atom value and an updating function as a tuple, just like useState. It takes an atom config created with atom(). Initially, there is no value stored in the Provider. The first time the atom is used via useAtom, it will add an initial value in the Provider. If the atom is a derived atom, the read function is executed to compute an initial value. When an atom is no longer used, meaning all the components using it is unmounted, and the atom config no longer exists, the value is removed from the Provider.

useAtomフックは、Providerに格納されたatomの値を読み込むためのフックです。useStateと同じように、atomの値と更新関数をタプルで返します。atom()で作成したatom configを受け取ります。初期状態では、Providerに値は保存されていません。useAtomによってatomが初めて使用されると、Providerに初期値が追加されます。atomが派生atomであれば、初期値を計算するために read 関数が実行されます。atomが使用されなくなると、つまり、atomを使用しているすべてのコンポーネントがアンマウントされ、atom configが存在しなくなると、その値はプロバイダから削除されます。

const [value, updateValue] = useAtom(anAtom)

The updateValue takes just one argument, which will be passed to the third argument of writeFunction of the atom. The behavior totally depends on how the writeFunction is implemented.

updateValue は1つの引数を取るだけで、atomの writeFunction の第3引数に渡されます。その動作は、writeFunction がどのように実装されるかに完全に依存します。

Notes

How atom dependency works

To begin with, let's explain this. In the current implementation, every time we invoke the "read" function, we refresh the dependencies and dependents. For example, If A depends on B, it means that B is a dependency of A, and A is a dependent of B.

まず始めに、こちらを説明しましょう。現在の実装では、"read "関数を呼び出すたびに、依存関係と被依存関係をリフレッシュしている。例えば、AがBに依存している場合、BはAの依存関係であり、AはBの依存関係であることを意味します。

const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())

The read function is the first parameter of the atom. The dependency will initially be empty. On first use, we run the read function and know that uppercaseAtom depends on textAtom. textAtom has a dependency on uppercaseAtom. So, add uppercaseAtom to the dependents of textAtom. When we re-run the read function (because its dependency (=textAtom) is updated), the dependency is built again, which is the same in this case. We then remove stale dependents and replace with the latest one.

読み出し関数は、atomの最初のパラメータです。依存関係は最初は空です。最初の使用時に、read関数を実行して、uppercaseAtomtextAtomに依存していることがわかります。textAtomuppercaseAtom に依存しています。そこで、upppercaseAtomtextAtom の依存関係に追加します。read関数を再実行すると(依存関係(=textAtom)が更新されるため)、依存関係が再び構築されることになりますが、この場合も同じです。そして、古くなった依存関係を削除して、最新のものに置き換えます。

Atoms can be created on demand

基本はグローバルに定義しますが、コンポーネント内で動的に作ることも可能です。そのための注意点が書かれています。

Basic examples in readme only show defining atoms globally outside components. There is no restrictions about when we create an atom.
As long as we know atoms are identified by their object referential identity,
it's okay to create them at anytime.

readmeにある基本的な例では、コンポーネントの外側でatomをグローバルに定義することだけを示しています。atomを作成するタイミングに制限はありません。
atomはそれらのオブジェクトの参照同一性で識別されることが分かっている限りは、いつ作っても問題ありません。

If you create atoms in render functions, you would typically want to use
a hook like useRef or useMemo for memoization. If not, the atom would be created everytime the component renders.

レンダー関数でatomを作成する場合、一般的には以下のものを使用します。
useRefuseMemoのようなフックを使用して、メモ化できます。そうしないと、コンポーネントがレンダリングするたびにatomが生成されてしまいます。

You can create an atom and store it with useState or even in another atom. See an example in issue #5.

atomを作成し、それを useState に格納したり、別のatomに格納することもできます。issue #5の例を参照してください。

You can cache atoms somewhere globally. See this example or
that example.

atomをグローバルにどこにでもキャッシュすることが出来ます. こちらの例
こちらの例を参照してください。

Check atomFamily in utils for parameterized atoms.

パラメータ化されたatomについて、utils の atomFamily をチェックしてください。

Some more notes about atoms

  • If you create a primitive atom, it will use predefined read/write functions to emulate useState behavior.
  • If you create an atom with read/write functions, they can provide any behavior with some restrictions as follows.
  • read function will be invoked during React render phase, so the function has to be pure. What is pure in React is described here.
  • write function will be invoked where you called initially and in useEffect for following invocations. So, you shouldn't call write in render.
  • When an atom is initially used with useAtom, it will invoke read function to get the initial value, this is recursive process. If an atom value exists in Provider, it will be used instead of invoking read function.
  • Once an atom is used (and stored in Provider), it's value is only updated if its dependencies are updated (including updating directly with useAtom).
  • プリミティブなatomを作成すると、あらかじめ定義されたread/write関数を使用して useState の挙動をエミュレートします。
  • read/write関数でatomを作成すると、以下のようにいくつかの制限付きで任意の動作を提供することができます。
  • read 関数は Reactのレンダーフェーズで呼び出されるため、純粋な関数である必要があります。Reactにおけるピュアとは何かについてはこちらに記載されています。
  • write 関数は、最初に呼び出した場所と、それ以降に呼び出す useEffect の中で呼び出されます。そのため、レンダリング中に write を呼び出すべきではありません。
  • useAtom でatomを最初に使用する場合、初期値を取得するために read 関数を呼び出しますが、これは再帰的な処理となります。もし、プロバイダにatomの値が存在すれば、 read 関数を呼び出す代わりに、その値が使用されます。
  • atomが使用されると(そしてプロバイダに格納されると)、その依存関係が更新された場合にのみその値が更新されます(useAtomで直接更新することも含む)。

Jotaiの紹介特集について

この記事はJotai FriendsによるJotai紹介特集記事の1つです。記事一覧はこちらからどうぞ。

Jotai Friendsとは

いちJotaiファンとして、エンジニアの皆さんにもっとJotaiを知ってもらって使ってもらいたい、そんな思いから立ち上げたのがJotai Friendsです。

https://jotaifriends.dev/

現在まだまだ準備中ですが今後ともよろしくお願いします!
(ご興味持っていただけた方は是非jotaifriends.devにてEメールアドレスのご登録をお願いします🙏)

Jotai Friends

Discussion