Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jotai #193

Open
hysryt opened this issue Dec 19, 2021 · 8 comments
Open

Jotai #193

hysryt opened this issue Dec 19, 2021 · 8 comments

Comments

@hysryt
Copy link
Owner

hysryt commented Dec 19, 2021

https://jotai.org/

@hysryt
Copy link
Owner Author

hysryt commented Dec 22, 2021

Recoilにインスパイアされている。
全てのステートはグローバルにアクセスできる。
AtomをlocalStrageに保存するのも簡単。

Recoilとの違い

  • ミニマムなAPI
  • string key 不要
  • TypeScript指向

@hysryt
Copy link
Owner Author

hysryt commented Dec 22, 2021

インストール

npx create-react-app my-app
cd my-app
npm install jotai

@hysryt
Copy link
Owner Author

hysryt commented Dec 22, 2021

アトム

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

const App = () => {
  const [count, setCount] = useAtom(countAtom);
  return (
    <p>{count}</p>
  );
};

アトムからアトムを導出する

const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);

const App = () => {
  const [count, setCount] = useAtom(doubleCountAtom);
  return (
    <p>{count}</p>
  );
}

複数のアトムからアトムを導出

const count1 = atom(1)
const count2 = atom(2)
const count3 = atom(3)

const sum = atom((get) => get(count1) + get(count2) + get(count3))

@hysryt
Copy link
Owner Author

hysryt commented Dec 27, 2021

コンセプト

Jotaiは、Reactの余分な再レンダリングの問題を解決するために生まれました。余分な再レンダリングとは、同じUI結果を生成するレンダリング処理で、ユーザーには何の違いも見えません。

Reactのコンテキスト(useContext + useState)で素朴にこの問題に取り組もうとすると、おそらく多くのコンテキストが必要になり、いくつかの問題に直面することになるでしょう。

  • Provider地獄:ルートコンポーネントが多くのコンテキストプロバイダを持っている可能性があります。これは技術的には問題なく、時には異なるサブツリーでコンテキストを提供することが望ましいです。
  • 動的な追加/削除:実行時に新しいコンテキストを追加するのは、新しいプロバイダを追加する必要があり、その子も再マウントされるため、あまり良いとは言えません。

従来、これに対するトップダウン的な解決策として、セレクタ・インターフェースを用いる方法がありました。use-context-selectorライブラリはその一例です。この方法の問題点は、セレクタ関数が再レンダリングを防ぐために参照的に等しい値を返す必要があり、多くの場合、何らかのメモ化技術が必要になることです。

Jotaiは、Recoilにインスパイアされたアトミックモデルによるボトムアップアプローチを採用しています。アトムを組み合わせて状態を構築し、アトムの依存性に基づいてレンダリングが最適化されます。これにより、メモ化技術を必要としません。

Jotaiには2つの理念があります。

  • プリミティブ:基本的なインターフェースはuseStateとほぼ同じです。
  • フレキシブル:導出アトムは他のアトムを組み合わせることができ、また副作用のあるuseReducerスタイルも可能です。

JotaiのコアAPIはミニマルであり、これをベースに様々なユーティリティを構築することが可能です。

他のライブラリとの違いを見るには、比較ドキュメントをご覧ください。

@hysryt
Copy link
Owner Author

hysryt commented Dec 30, 2021

プリミティブ

Jotaiにおける状態は、atomの集合体です。atomは状態の一部分です。ReactのuseStateとは異なり、atomは特定のコンポーネントに縛られることはありません。では、atomの定義と使い方をみていきましょう。

atom

atomというエクスポートされた関数がありますが、これはatom configを作成するためのものです。これは単なる定義で、値を保持しないので「config」と呼んでいます。文脈的に明確な場合は、単に「atom」と呼ぶこともあります。
プリミティブなatom(config)を作るには、初期値を渡すだけです。

import { atom } from 'jotai'

const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

また、導出atomを作ることも可能です。3つのパターンを用意しています。

  • 読み込み専用atom
  • 書き込み専用atom
  • 読み書き可能atom

導出atomを作成するには、読み込み関数とオプションの書き込み関数を渡します。

const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
  null, // 第一引数にはnullを渡す
  (get, set, update) => {
    // updateは、このatomを更新するために受け取る任意の値
    set(priceAtom, get(priceAtom) - update.discount)
  }
)
const readWriteAtom = atom(
  (get) => get(priceAtom) * 2,
  (get, set, newPrice) => {
    set(priceAtom, newPrice / 2)
    // 同時にいくつでもアトムを設定することができる
  }
)

読み込み関数のgetはatomを読み取るものです。リアクティブであり、依存関係はトラッキングされます。

書き込み関数の get もatom値を読み込むものですが、トラッキングはされません。さらに、未解決の非同期値は読めません。非同期の動作については、asyncのドキュメントを参照してください。

書き込み関数の set は、atomの値を書き込むためのものです。対象atomの書き込み関数が呼び出されます。

atom configはどこでも作成可能ですが、参照一致が重要です。また、動的に作成することもできます。render関数でatomを作成するには、安定した参照を得るために useMemo または useRef が必要です。useMemoかuseRefを使うか迷ったら、useMemoを使いましょう。

const Component = ({ value }) => {
  const valueAtom = useMemo(() => atom({ value }), [value])
  // ...
}

useAtom

useAtomフックは、状態にあるatom値を読み込むためのものです。状態は、atom configとatom値のWeakMapとして見ることができます。

useAtom関数は、ReactのuseStateと同様に、atom値と更新関数をタプルとして返します。引数としてatom()で作成したatom configを受け取ります。

初期状態では、状態には値が格納されていません。useAtomによりatomが初めて使用されたとき、初期値がステートに格納されます。atomが導出atomの場合は、読み込み関数が実行され、初期値が計算されます。atomが使用されなくなったとき、つまりatomを使用しているコンポーネントがすべてアンマウントされ、atom構成が存在しなくなったとき、状態の値はガベージコレクションされます。

const [value, updateValue] = useAtom(anAtom)

updateValueは引数を1つだけ取り、atomのwrite関数の第3引数に渡されます。動作は書き込み関数がどのように実装されるかに完全に依存します。

Provider

Providerとは、コンポーネントのサブツリーに対して状態を提供するものです。複数のサブツリーに対して複数のProviderを使用することができ、入れ子にすることも可能です。これは、通常のReact Contextと同じように動作します。

Providerが存在しないツリーでatomが使用された場合、デフォルトの状態が使用されます。これはいわゆるProvider-lessモードです。

プロバイダーがいくつかの点で便利です。

  1. サブツリーごとに異なる状態を提供することができる
  2. Provicerは、いくつかのデバッグ情報を保持することができる。
  3. Providerはatomの初期値を受け取ることができる。
const SubTree = () => (
  <Provider>
    <Child />
  </Provider>
)

@hysryt
Copy link
Owner Author

hysryt commented Dec 31, 2021

Async

Jotaiでは非同期サポートは第一級です。React Suspenseをフルに活用しています。

技術的には、React.lazy以外のSuspenseの使い方はReact 17ではまだ未サポート/未ドキュメントの状態です。もしブロックされているようなら、guides/no-suspenseをチェックしてみてください。

Suspense

非同期atomを使用するには、コンポーネントツリーをで囲む必要があります。がある場合、少なくとも1つのはの中に配置されます。

const App = () => (
  <Provider>
    <Suspense fallback="Loading...">
      <Layout />
    </Suspense>
  </Provider>
)

コンポーネントツリーでより多くのを持つことは可能です。

atomの非同期読み込み

atomの読み込み関数は、Promiseを返すことができます。Promiseが達成されると、一時停止して再レンダリングします。

最も重要なことは、useAtomは解決された値しか返さないということです。

const countAtom = atom(1)
const asyncCountAtom = atom(async (get) => get(countAtom) * 2)
// 読み込み関数はPromiseを返す

const Component = () => {
  const [num] = useAtom(asyncCountAtom)
  // num は数値であることが保証されている
}

atomは、読み取り関数が非同期であるだけでなく、その依存関係の1つ以上が非同期である場合に非同期となります。

const anotherAtom = atom((get) => get(asyncCountAtom) / 2)
// このatomはPromiseを返さないが、
// `asyncCountAtom`が非同期なので、非同期読み出しatomとなる。

@hysryt
Copy link
Owner Author

hysryt commented Jan 9, 2022

比較

Zustandとの違い

https://zustand.surge.sh/

名前

Jotaiは日本語で「状態」を意味します。Zustandはドイツ語で「状態」を意味します。

類似性

JotaiはRecoilに近い。ZustandはReduxに近い。

ステートが存在する場所

JotaiのステートはReactのコンポーネントツリーの中にあります。ZustandのステートはReactの外側のストアにあります。

ステートの構成方法

Jotaiのステートはatomからなる(ボトムアップ)。Zustandのステートは1つのオブジェクト(つまりトップダウン)。

技術的な違い

大きな違いは、ステートモデルです。Zustandは基本的に1つのストアです(複数のストアを作ることもできますが、分離されています)。Jotaiはプリミティブなアトムで、それを合成します。その意味では、プログラミングのメンタルモデルの問題ですね。
JotaiはuseState+useContextの置き換えと見ることができる。複数のコンテキストを作る代わりに、アトムは一つの大きなコンテキストを共有します。
Zustandは外部のストアであり、フックは外部の世界とReactの世界を繋ぐためのものです。

いつ、どれを使うか

  • useState+useContextの代替が必要な場合、Jotaiはよく合います。
  • Reactの外側で状態を更新したい場合は、Zustandの方が効果的です。
  • コード分割を重視するならば、Jotaiは良いパフォーマンスを発揮するはずです。
  • Reduxのdevtoolsを好むなら、Zustandは良い。
  • Suspenseを活かすならJotai。

Recoilとの違い

(免責事項:筆者はRecoilにあまり詳しくありません。偏りがあり、正確でない可能性があります)。

開発者

  • Jotaiは、Poimandres(旧リアクトスプリング)orgの数名の開発者による共同作業で開発されています。
  • Recoilは、Facebookのチームが開発したものです。

基本情報

  • JotaiはリーンラーニングのためのプリミティブなAPIに力を入れていて、独創的です。(Zustandと同じ思想)
  • Recoilは、様々な要件を持つ大きなアプリのためにフル機能を備えています。

技術的な違い

  • Jotaiはatomオブジェクトの参照同一性に依存しています。
  • Recoilはatom string keyに依存します。

いつ、どれを使うか

  • 新しいことを学びたいなら、どちらでもいいはずです。
  • Zustandが好きなら、Jotaiも心地よいだろう。
  • もし、あなたのアプリがステートのシリアライゼーション(ストレージ、サーバー、URLへのステートの保存)を大きく必要とするならば、Recoilは良い機能を備えています。
  • React Contextの代替が必要なら、Jotaiは十分な機能を備えています。
  • もし、新しいライブラリを作ろうとするならば、Jotaiは良いプリミティブを与えてくれるかもしれません。
  • その他、大まかな目標や基本的なテクニックについては、どちらもよく似ていますので、ぜひ両方試してみて、ご意見をお聞かせください。

@hysryt
Copy link
Owner Author

hysryt commented Feb 12, 2022

Jotai 1.6.0

useUpdateAtomuseSetAtom にリネームし、 jotai/util からコアに移動

https://jotai.org/docs/utils/use-update-atom

useAtom が値とセット関数を取得するのに対し、useSetAtom はセット関数のみを取得する。
アトムの値に応じて再レンダリングの必要がない場合は useSetAtom を使用した方が良い。

Recoil の useSetRecoilState と同等と思われる。

useAtomValue が jotai/util からコアに移動

https://jotai.org/docs/utils/use-atom-value

useAtom が値とセット関数を取得するのに対し、useAtomValue はセット関数のみを取得する。

Recoil の useRecoilValue と同等と思われる。

これによって、Jotaiが公開するAPIは atomuseAtomProvideruserSetAtomuseAtom の5つとなった。

unstable_createStore を追加

Storeを作成する。
StoreはReact以外からも使用ができ、Atomの更新、取得、サブスクリプションが可能。

import { atom, unstable_createStore } from "jotai";

const countAtom = atom(0);
const store = unstable_createStore();

store.sub(countAtom, () => {
    const value = store.get(countAtom);
    console.log('更新: ' + value);
});

store.set(countAtom, 1);  // 更新: 1
store.set(countAtom, c => c + 1);  // 更新: 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant