React状態管理ライブラリ Jotai のCore API #JotaiFriends
こちらで紹介している公式の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 ofread
is(get) => Value | Promise<Value>
, andget
is a function that takes an atom config and returns its value stored in Provider described below. Dependency is tracked, so ifget
is used for an atom at least once, theread
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 ofuseAtom
, theuseAtom()[1]
. The default value of this function in the primitive atom will change the value of that atom. The signature ofwrite
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 byuseAtom
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. Thewrite
of primitive atoms is equivalent to thesetState
ofReact.useState
.atomには、書き込み可能なatomと読み取り専用のatomの2種類があります。プリミティブatomは常に書き込み可能です。派生atomは
write
が指定された場合、書き込み可能です。プリミティブatomのwrite
はReact.useState
のsetState
と等価です。
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 functionsetAtom
and returnsonUnmount
function optionally.作成されたatom configは、オプションプロパティの
onMount
を持つことができます。onMount
は関数setAtom
を受け取り、オプションでonUnmount
関数を返します。
The
onMount
function will be invoked when the atom is first used in a provider, andonUnmount
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’swrite
. Customizingwrite
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 viauseAtom
, 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 ontextAtom
.textAtom
has a dependency onuppercaseAtom
. So, adduppercaseAtom
to the dependents oftextAtom
. 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関数を実行して、
uppercaseAtom
がtextAtom
に依存していることがわかります。textAtom
はuppercaseAtom
に依存しています。そこで、upppercaseAtom
をtextAtom
の依存関係に追加します。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 likeuseRef
oruseMemo
for memoization. If not, the atom would be created everytime the component renders.レンダー関数でatomを作成する場合、一般的には以下のものを使用します。
useRef
やuseMemo
のようなフックを使用して、メモ化できます。そうしないと、コンポーネントがレンダリングするたびに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.
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 callwrite
in render.- When an atom is initially used with
useAtom
, it will invokeread
function to get the initial value, this is recursive process. If an atom value exists in Provider, it will be used instead of invokingread
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です。
現在まだまだ準備中ですが今後ともよろしくお願いします!
(ご興味持っていただけた方は是非jotaifriends.devにてEメールアドレスのご登録をお願いします🙏)
Jotai Friends のテックブログです。 React状態管理ライブラリJotai(jotai.org)を新たな選択肢に。 エンジニアの皆さんが安心してJotaiを採用できるように支援します。 Powered by twitter.com/t6adev
Discussion