🧙

Valtioはどのように生まれたか、3つ目のライブラリは必要だったのか?

2024/10/01に公開

こんにちは、Valtioの作者です。Zustand、Jotaiに続いて3つ目のライブラリも開発したわけですが、Valtioが生まれた経緯を記事にしてますので、よろしければご覧ください。

https://blog.axlight.com/posts/how-valtio-was-born/

以下、ChatGPTによる翻訳です。


はじめに

Zustand v3と新しいJotaiをリリースした後、チーム内でグローバルステート用の別のライブラリを開発できるかどうかについて議論がありました。

この投稿では、Valtioの開発の始まりとそのAPIデザインについて振り返ります。

最初のためらい

プロキシステートのアイデアは魅力的に思えましたが、最初は3つ目のグローバルステートライブラリを開発することにためらいがありました。同じ問題領域にすでに2つのライブラリを維持していたので、さらに1つ追加するのは意味がないように感じたのです。

もう1つの懸念は、MobXがすでに存在していることで、競争できるかどうか確信が持てなかったことです。特に、何が根本的な違いになるのかがわかりませんでした。

解決すべき課題

Reactでミュータブルな状態を使用する際に課題がありました。Reactの契約は不変性に基づいており、単純にミュータブルな状態を使うことは、特に並行性に関してはReactではうまくいきません。不変な状態がなければ、フックAPIは実現できません。この問題はReact Trackedの開発中にすでに知っており、MobXチームに彼らのフックAPIのための潜在的な解決策を提案したこともありました。しかし、彼らはその道を選ばず、HoC APIに固執しました。

これが、ミュータブルな状態に対してフックAPIを提供することができれば、MobXとは大きな違いになると考えたきっかけでした。

目標はシンプルで、state.count++のような構文をサポートし、Reactのためのフックを提供することでした。

もう一つの動機

先ほど述べたように、私はReact Trackedというライブラリを開発しました。

https://react-tracked.js.org

これはある程度プロダクションで使われていますが、プロキシの使用が一部のユーザーにとって採用の妨げとなりました。彼らはプロキシを使わない代替の内部ライブラリであるuse-context-selectorを好んでいました。

もしプロキシステートを使った新しいライブラリを作ることができれば、React Trackedと同じ機能を提供する良い機会になるかもしれません。すでにプロキシベースなので、プロキシの使用が障害にはなりません。幸いなことに、私はReact Tracked用にproxy-compareという内部ライブラリを既に開発していました。これは状態の使用を追跡するための基本的なライブラリで、新しいライブラリでも使えるものです。

Valtioの誕生

私の目標は、内部ライブラリproxy-compareを活用してフックAPIを備えたミュータブルステートをサポートするライブラリを開発することでした。キーとなるアイデアは「スナップショット」でした。Reactフックと連携させるために、ミュータブルな状態から不変なオブジェクトを作成する必要がありました。私はそれをスナップショットと呼んでいます。この不変なオブジェクトを使えば、proxy-compareは問題なく動作します。

最初のプロトタイプは1日か2日で開発しました。APIは非常にシンプルで、すべてが裏で動作しました。結果にとても興奮しました。

https://github.com/pmndrs/valtio

ValtioのAPIデザイン

APIについて言えば、私たちのデザイン哲学は「ただのJavaScriptのように感じられること」です。state.count++が動作するようにしたかったのです。つまり、state.countで値を読み取り、state.count = 1で書き込むことができるということです。基本的に、それがプロキシであるかどうかは気づかないでしょう。

例えば、state.count.get()state.count()のようなAPIは受け入れられません。プロキシ状態を購読する場合も同様です。ライブラリはsubscribeという関数をエクスポートしており、次のように使うことができます:

subscribe(state, () => {
  console.log(state.count);
});

状態オブジェクトにメソッドを持たせて、次のように使うことはできません:

state.subscribe(() => {
  console.log(state.count);
});

これは名前の衝突を引き起こす可能性があるためです。シンボルを使って次のようにすることもできました:

state[SUBSCRIBE](() => {
  console.log(state.count);
});

しかし、エクスポートされた関数の方が明示的です。実際には内部でシンボルを使用しています。

また、エクスポートされたsubscribe関数のもう一つの利点は、縮小可能であることです。ValtioのAPIと実装は、可能な限り縮小可能なように設計されています。

スナップショット作成の秘密のソース

スナップショットのアイデアはそれほど難しくありませんが、実装はさまざまなシナリオをカバーしています:

  • ネストされたオブジェクト(これは当然)
  • 遅延評価
  • 再評価を避けるためのキャッシング
  • 循環参照のサポート

ImmutableJSを覚えているなら、その実装デザインはおそらく似ています。

先ほど述べたように、スナップショットのアイデアはReact契約だけでなく、proxy-compareライブラリにとっても重要です。内部的に、Valtioは2つの部分で構成されています:valtio/vanillaはプロキシステートとスナップショットを作成し、valtio/reactproxy-compareと共にフックAPIを提供します。

興味がある方は、私のブログ投稿もぜひご覧ください:

現在の状況

Valtioはしばらくの間存在しており、プロダクションで広く使われています。現在のバージョンv1はかなり安定していますが、まもなくv2をリリースする予定です。これは、v1からのフィードバックに対処し、いくつかの破壊的な変更を必要とするものや、新しいuseフックを導入するReact 19との互換性を確保するためのものです。

Discussion