Recoil は、 Reactの世界にアトミック モデルを導入しました。その新しい能力は、急な学習曲線と乏しい学習リソースという犠牲を払って実現されました。
その後、 JotaiとZedux はこの新しいモデルのさまざまな側面を簡素化し、多くの新機能を提供し、この驚くべき新しいパラダイムの限界を押し広げました。
他の記事では、これらのツールの違いに焦点を当てます。この記事では、3 つすべてに共通する 1 つの大きな機能に焦点を当てます。
彼らはFluxを修正しました。
Flux を知らない方のために、簡単な要点を以下に示します。
Reduxに加えて、すべての Flux ベースのライブラリは基本的にこのパターンに従いました。アプリには複数のストアがあります。すべてのストアに適切な順序でアクションをフィードすることを仕事とする Dispatcher は 1 つだけあります。この「適切な順序」とは、ストア間の依存関係を動的に整理することを意味します。
たとえば、e コマース アプリのセットアップを考えてみましょう。
ユーザーが、たとえばバナナをカートに移動すると、PromosStore は、利用可能なバナナ クーポンがあるかどうかを確認するリクエストを送信する前に、CartStore の状態が更新されるのを待つ必要があります。
あるいは、バナナをユーザーの地域に発送できない可能性もあります。 CartStore は更新する前に UserStore をチェックする必要があります。あるいは、クーポンは週に 1 回しか使用できない場合もあります。 PromosStore は、クーポン リクエストを送信する前に UserStore をチェックする必要があります。
Flux はこれらの依存関係を好みません。 従来の React ドキュメントから:
Flux アプリケーション内のオブジェクトは高度に分離されており、システム内の各オブジェクトがシステム内の他のオブジェクトについてできる限り知らなければならないという原則であるデメテルの法則に非常に強く準拠しています。
この背後にある理論はしっかりしています。 100%。そうですね...なぜこの複数店舗で展開されていたフラックスのフレーバーは消滅したのでしょうか?
結局のところ、分離状態のコンテナ間の依存関係は避けられないことがわかりました。実際、コードをモジュール化して DRY に保つには、他のストアを頻繁に使用する必要があります。
Flux では、これらの依存関係はオンザフライで作成されます。
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
この例は、依存関係がストア間で直接宣言されず、アクションごとに結合される方法を示しています。これらの非公式な依存関係を見つけるには、実装コードを徹底的に調べる必要があります。
これは非常に簡単な例です。しかし、フラックスがどのように感じているかはすでにわかります。副作用、選択操作、および状態の更新はすべて一緒に石畳まれています。このコロケーションは実際にはちょっといいかもしれません。しかし、いくつかの非公式な依存関係を混ぜ合わせ、レシピを 3 倍にして定型文に盛り込むと、Flux がすぐに機能しなくなることがわかります。
FlummoxやRefluxなどの他の Flux 実装により、ボイラープレートとデバッグ エクスペリエンスが向上しました。非常に便利ですが、依存関係の管理はすべての Flux 実装を悩ませていた唯一の厄介な問題でした。他の店を利用するのは気持ち悪いと感じました。深くネストされた依存関係ツリーを追跡するのは困難でした。
この e コマース アプリには、いつか OrderHistory、 ShippingCalculator、DeliverEstimate、BananasHoarded などのストアが含まれる可能性があります。大規模なアプリには、簡単に数百のストアが含まれる可能性があります。すべてのストアで依存関係を最新の状態に保つにはどうすればよいでしょうか?副作用をどのように追跡しますか?純度についてはどうですか?デバッグについてはどうですか?バナナは本当にベリーですか?
Flux によって導入されたプログラミング原則に関しては、一方向のデータ フローが勝者でしたが、今のところ、デメテルの法則は勝者ではありませんでした。
Redux がどのようにして窮地を救おうと猛然と登場したのかは誰もが知っています。複数のストアの概念を捨て、シングルトン モデルを採用しました。これで、すべてが「依存関係」をまったく持たずに他のすべてのものにアクセスできるようになります。
Reducer は純粋であるため、複数の状態スライスを処理するすべてのロジックはストアの外に出す必要があります。コミュニティは副作用と派生状態を管理するための標準を作成しました。 Redux ストアは見事にデバッグ可能です。 Redux が当初修正できなかった唯一の主要な Flux 欠陥は、定型文でした。
RTK は後に Redux の悪名高い定型文を簡素化しました。その後、 Zustand はデバッグ能力をいくらか犠牲にして綿毛を取り除きました。これらのツールはすべて、React の世界で非常に人気があります。
モジュール状態では、依存関係ツリーが非常に自然に複雑になるため、私たちが考える最善の解決策は、「それはやめておこう」というものでした。
そしてそれはうまくいきました!この新しいシングルトン アプローチは、ほとんどのアプリで依然として十分に機能します。 Flux の原則は非常にしっかりしていたので、依存関係の悪夢を取り除くだけで問題は解決されました。
それともできましたか?
シングルトンアプローチの成功により、そもそも Flux は何を目指していたのかという疑問が生じます。なぜ複数の店舗が必要になったのでしょうか?
これについて少し説明させてください。
複数のストアでは、状態の部分が独自の自律的なモジュール式コンテナーに分割されます。これらのストアは個別にテストできます。アプリとパッケージ間で簡単に共有することもできます。
これらの自律ストアは、個別のコード チャンクに分割できます。ブラウザでは、遅延ロードしてオンザフライでプラグインできます。
Redux のリデューサーは、コード分割も非常に簡単です。 replaceReducer
のおかげで、追加の手順は新しい結合 Reducer を作成することだけです。ただし、副作用やミドルウェアが関係する場合は、さらに多くの手順が必要になる場合があります。
シングルトン モデルでは、外部モジュールの内部状態を自分の内部状態と統合する方法を知るのは困難です。 Redux コミュニティは、これを解決する試みとして Ducks パターンを導入しました。そして、それは少し定型的なものを犠牲にして機能します。
複数のストアがある場合、外部モジュールは単純にストアを公開できます。たとえば、フォーム ライブラリは FormStore をエクスポートできます。この利点は、標準が「公式」であること、つまり人々が独自の方法論を作成する可能性が低いことです。これにより、より堅牢で統合されたコミュニティとパッケージのエコシステムが実現します。
シングルトン モデルは驚くほどパフォーマンスが優れています。 Redux はそれを証明しました。ただし、その選択モデルには特に厳しい上限があります。これについては、この再選択のディスカッションでいくつかの考えを書きました。大きくて高価なセレクター ツリーは、キャッシュを最大限に制御している場合でも、実際には問題が発生する可能性があります。
一方、複数のストアでは、ほとんどの状態更新は状態ツリーの小さな部分に分離されます。システム内の他のものには一切触れません。これは、シングルトンのアプローチをはるかに超えてスケーラブルです。実際、複数のストアがある場合、ユーザーのマシンのメモリ制限に達する前に CPU の制限に達するのは非常に困難です。
Redux では状態を破棄することはそれほど難しくありません。コード分割の例と同様に、リデューサー階層の一部を削除するには、いくつかの追加手順のみが必要です。しかし、複数のストアを使用すると、さらに簡単になります。理論的には、ストアをディスパッチャから切り離すだけで、ガベージ コレクションできるようになります。
これは、Redux、Zustand、およびシングルトン モデル一般ではうまく処理できない大きな問題です。副作用は、相互作用する状態から分離されます。選択ロジックはすべてから分離されています。複数店舗の Flux はおそらく同じ場所にありすぎましたが、Redux はその逆の極端な方向に進みました。
複数の自律型店舗があれば、これらのことは自然に連携します。実際のところ、Flux には、すべてがごちゃごちゃの寄せ集めになってしまわないようにするための基準がいくつか不足しているだけでした (申し訳ありません)。
さて、OG Flux ライブラリをご存知の方は、実際にはこれらすべての点で優れたものではなかったことをご存知でしょう。ディスパッチャーは依然としてグローバルなアプローチを採用しており、すべてのアクションをすべてのストアにディスパッチします。また、非公式/暗黙的な依存関係を伴う全体的なコードの分割と破壊も完璧ではありませんでした。
それでも、Flux には素晴らしい機能がたくさんありました。さらに、複数ストアのアプローチには、制御の反転やフラクタル (別名ローカル) 状態管理など、さらに多くの機能が追加される可能性があります。
誰かが女神デメテルと名付けていなければ、フラックスは真に強力な国家管理者に進化していたかもしれない。私は真剣です! ...わかりました、違います。しかし、そう言われてみると、デメテルの法則はもっと詳しく調べてみる価値があるかもしれません。
このいわゆる「法」とは一体何なのでしょうか?ウィキペディアより:
- 各ユニットは、他のユニットについて限られた知識のみを持つ必要があります。つまり、現在のユニットに「密接に」関連するユニットのみです。
- 各ユニットはその友人とのみ会話する必要があります。見知らぬ人と話さないでください。
この法律はオブジェクト指向プログラミングを念頭に置いて設計されましたが、React の状態管理を含む多くの分野に適用できます。
基本的な考え方は、ストアが次のことを行わないようにすることです。
バナナの用語で言えば、バナナは別のバナナの皮をむいてはならず、別の木のバナナと会話してはなりません。ただし、2 つのツリーが最初にバナナ電話回線を設置すれば、もう一方のツリーと通信できます。
これにより、懸念事項の分離が促進され、コードがモジュール化され、DRY かつ SOLID に保たれるようになります。確かな理論!では、フラックスには何が欠けていたのでしょうか?
ストア間の依存関係は、優れたモジュール式システムの自然な部分です。ストアが別の依存関係を追加する必要がある場合は、それをできるだけ明示的に行う必要があります。 Flux コードの一部をもう一度示します。
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore には、さまざまな方法で宣言された複数の依存関係がありますCartStore
を待機して読み取り、またUserStore
から読み取ります。これらの依存関係を発見する唯一の方法は、PromosStore の実装でストアを探すことです。
開発ツールは、これらの依存関係をより発見しやすくするのにも役立ちません。言い換えれば、依存関係が暗黙的すぎるということです。
これは非常に単純で不自然な例ですが、フラックスがデメテルの法則をどのように誤解したかを示しています。これは主に Flux の実装を小さく保ちたいという願望から生まれたものだと私は確信していますが (実際の依存関係管理は複雑な作業です!)、ここが Flux が足りなかった点です。
この物語の主人公たちとは異なります。
2020 年、 Recoil は偶然そのシーンに登場しました。最初は少しぎこちなかったものの、Flux の複数店舗アプローチを復活させる新しいパターンを教えてくれました。
一方向のデータ フローがストア自体から依存関係グラフに移動しました。ストアはアトムと呼ばれるようになりました。アトムは適切に自律的であり、コード分割可能でした。彼らはサスペンスサポートや水分補給などの新しい能力を持っていました。そして最も重要なことは、アトムがその依存関係を正式に宣言することです。
原子模型が誕生しました。
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil は、コードベースの肥大化、メモリ リーク、パフォーマンスの低下、開発の遅さ、機能の不安定さ (最も顕著な副作用) に悩まされていました。これらのいくつかは徐々に解決されていきますが、それまでの間、他のライブラリが Recoil のアイデアを採用して実行しました。
Jotai はシーンに登場し、すぐに支持者を獲得しました。
// a Jotai atom const greetingAtom = atom('Hello, World!')
Jotai は、Recoil のサイズに比べてごくわずかなサイズであることに加えて、WeakMap ベースのアプローチにより、より優れたパフォーマンス、より洗練された API、およびメモリ リークを提供しませんでした。
ただし、これにはある程度の電力が犠牲になりました。WeakMap アプローチではキャッシュ制御が困難になり、複数のウィンドウまたは他のレルム間で状態を共有することがほとんど不可能になります。また、文字列キーがないため、洗練されていますが、デバッグは悪夢のようなものになります。ほとんどのアプリはこれらを再度追加する必要があり、Jotai の洗練された外観が大幅に損なわれます。
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
いくつかの佳作としては、 ReatomとNanostoresがあります。これらのライブラリは、原子モデルの背後にある理論をさらに調査し、そのサイズと速度を限界まで押し上げることを試みています。
アトミック モデルは高速で、拡張性も非常に優れています。しかし、ごく最近まで、どのアトミック ライブラリもうまく対処できなかったいくつかの懸念がありました。
学習曲線。原子は異なります。これらの概念を React 開発者にとって親しみやすいものにするにはどうすればよいでしょうか?
Dev X とデバッグ。原子を発見可能にするにはどうすればよいでしょうか?更新情報を追跡したり、適切な実践を実施したりするにはどうすればよいですか?
既存のコードベースの増分移行。外部ストアにアクセスするにはどうすればよいですか?既存のロジックを維持するにはどうすればよいでしょうか?完全な書き換えを回避するにはどうすればよいでしょうか?
プラグイン。アトミックモデルを拡張可能にするにはどうすればよいでしょうか?あらゆる状況に対応できるでしょうか?
依存関係の注入。アトムは当然依存関係を定義しますが、テスト中または別の環境でアトムを交換することはできますか?
デメテルの法則。実装の詳細を非表示にし、散在的な更新を防ぐにはどうすればよいでしょうか?
ここが私の出番です。ほら、私は別のアトミック ライブラリの主な作成者です。
Zedux は数週間前についに登場しました。私が勤務しているニューヨークのフィンテック企業によって開発された Zedux は、高速かつスケーラブルであるだけでなく、洗練された開発およびデバッグ エクスペリエンスを提供するように設計されています。
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
ここでは Zedux の機能については詳しく説明しません。すでに述べたように、この記事ではこれらのアトミック ライブラリ間の違いには焦点を当てません。
Zedux は上記のすべての懸念に対処していると言えば十分でしょう。たとえば、これは実際の制御反転を提供する最初のアトミック ライブラリであり、実装の詳細を隠すためのアトム エクスポートを提供することで、デメテルの法則に完全に戻る最初のアトミック ライブラリです。
Flux の最後のイデオロギーがついに復活しました - 復活しただけでなく、改善されました! - 原子モデルのおかげです。
では、原子模型とは一体何なのでしょうか?
これらのアトミック ライブラリには多くの違いがあり、「アトミック」が意味する定義さえ異なります。一般的なコンセンサスは、アトムは小さく、孤立した自律状態のコンテナであり、有向非巡回グラフを介して反応的に更新されるということです。
複雑に聞こえるかもしれませんが、バナナで説明するまで待ってください。
冗談です!それは実際にはとても簡単です:
更新はグラフを通じて飛び交います。それでおしまい!
重要なのは、実装やセマンティクスに関係なく、これらのアトミック ライブラリはすべて、複数のストアの概念を復活させ、それらを使用できるだけでなく、操作するのに本当に楽しいものにしたということです。
複数のストアが必要な理由として私が挙げた 6 つの理由は、まさにアトミック モデルが非常に強力である理由です。
シンプルな API とスケーラビリティだけでも、アトミック ライブラリはあらゆる React アプリにとって優れた選択肢となります。 Redux よりも強力で、定型文が少ないのでしょうか?これは夢ですか?
なんと素晴らしい旅でしょう! React の状態管理の世界は常に驚きに満ちており、参加できて本当によかったと思っています。
まだ始まったばかりです。アトムには革新の余地がたくさんあります。 Zedux の作成と使用に何年も費やした後、私はアトミック モデルがいかに強力であるかを見てきました。実際、その力はアキレス腱です。
開発者がアトムを探索するとき、多くの場合、可能性を深く掘り下げるあまり、「アトムがどのように単純かつエレガントにこの問題を解決するかを見てください」ではなく、「このクレイジーで複雑なパワーを見てください」と言って戻ってきます。私はこれを変えるためにここにいます。
アトミック モデルとその背後にある理論は、ほとんどの React 開発者にとって親しみやすい方法で教えられていません。ある意味、これまでの React ワールドのアトムの経験は Flux の逆でした。
この記事は、React 開発者がアトミック ライブラリがどのように機能するのか、またアトミック ライブラリを使用する理由を理解できるようにするために私が作成している一連の学習リソースの 2 番目です。最初の記事「スケーラビリティ: React 状態管理の失われたレベル」を確認してください。
10 年かかりましたが、Flux によって導入された確かな CS 理論は、アトミック モデルのおかげで、最終的に React アプリに大きな影響を与えています。そしてそれは今後何年も続くでしょう。