hysryt / wiki

https://hysryt.github.io/wiki/
0 stars 0 forks source link

Jotai #193

Open hysryt opened 2 years ago

hysryt commented 2 years ago

https://jotai.org/

hysryt commented 2 years ago

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

Recoilとの違い

hysryt commented 2 years ago

インストール

npx create-react-app my-app
cd my-app
npm install jotai
hysryt commented 2 years ago

アトム

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 commented 2 years ago

コンセプト

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

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

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

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

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

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

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

hysryt commented 2 years ago

プリミティブ

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を作成するには、読み込み関数とオプションの書き込み関数を渡します。

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 commented 2 years ago

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 commented 2 years ago

比較

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の世界を繋ぐためのものです。

いつ、どれを使うか

Recoilとの違い

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

開発者

基本情報

技術的な違い

いつ、どれを使うか

hysryt commented 2 years ago

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