フラックスとバックボーンを使用したReactアプリの単純なデータフロー:例を含むチュートリアル

公開: 2022-03-11

React.jsは素晴らしいライブラリです。 Pythonをスライスして以来、それが最良のように思えることもあります。 ただし、Reactはフロントエンドアプリケーションスタックの一部にすぎません。 データと状態の管理に関しては、提供できるものはあまりありません。

ReactのメーカーであるFacebookは、Fluxの形でいくつかのガイダンスを提供しています。 Fluxは、Reactビュー、アクションディスパッチャー、およびストアを使用して一方向のデータフローを中心に構築された「アプリケーションアーキテクチャ」(フレームワークではありません)です。 Fluxパターンは、イベント制御の重要な原則を具体化することによっていくつかの主要な問題を解決します。これにより、Reactアプリケーションの推論、開発、および保守がはるかに簡単になります。

ここでは、制御フローの基本的なFluxの例を紹介し、ストアに欠けているもの、およびバックボーンモデルとコレクションを使用して「Flux準拠」の方法でギャップを埋める方法について説明します。

(注:便宜上、簡潔にするために、例ではCoffeeScriptを使用しています。CoffeeScript以外の開発者は、従うことができ、例を擬似コードとして扱うことができます。)

Facebookのフラックスの紹介

バックボーンは、ビュー、モデル、コレクション、およびルートを含む、優れた、よく吟味された小さなライブラリです。 これは構造化されたフロントエンドアプリケーションのデファクトスタンダードライブラリであり、2013年にReactアプリが導入されて以来、Reactアプリとペアになっています。これまでのFacebook.com以外のReactのほとんどの例には、Backboneがタンデムで使用されているという言及が含まれています。

残念ながら、Reactのビュー以外のアプリケーションフロー全体を処理するためにBackboneだけに頼ると、不幸な問題が発生します。 私が最初にReact-Backboneアプリケーションコードの作業を始めたとき、私が読んだ「複雑なイベントチェーン」は、ヒドラのような頭を育てるのにそれほど時間はかかりませんでした。 UIからモデルにイベントを送信し、次にあるモデルから別のモデルにイベントを送信してから再度送信すると、誰が誰を、どの順序で、なぜ変更したかを追跡するのが困難になります。

このFluxチュートリアルでは、Fluxパターンがこれらの問題を驚くほど簡単かつシンプルに処理する方法を示します。

概要

Fluxのスローガンは「一方向のデータフロー」です。 これは、そのフローがどのように見えるかを示すFluxドキュメントの便利な図です。

Facebook Fluxは、ReactおよびBackboneと組み合わせると少し変化する「一方向データフロー」モデルを使用します。

重要な点は、 React --> Dispatcher --> Stores --> Reactからコンテンツが流れることです。

各主要コンポーネントとは何か、およびそれらがどのように接続されているかを見てみましょう。

ドキュメントには、この重要な警告も記載されています。

Fluxはフレームワークというよりもパターンであり、強い依存関係はありません。 ただし、ストアのベースとしてEventEmitterを使用し、ビューのReactを使用することがよくあります。 他の場所ではすぐに利用できないフラックスの1つは、ディスパッチャーです。 このモジュールは、Fluxツールボックスを完成させるためにここで利用できます。

したがって、Fluxには3つのコンポーネントがあります。

  1. ビュー( React = require('react')
  2. Dispatcher( Dispatcher = require('flux').Dispatcher
  3. ストア( EventEmitter = require('events').EventEmitter
    • (または、すぐにわかるように、 Backbone = require('backbone')

ビュー

Reactについては多くのことが書かれているので、ここでは説明しません。AngularよりもReactの方が非常に好きだと言う以外はありません。 Angularとは異なり、Reactコードを書くときに混乱することはほとんどありませんが、もちろん意見はさまざまです。

ディスパッチャー

Flux Dispatcherは、ストアを変更するすべてのイベントが処理される単一の場所です。 これを使用するには、すべてのイベントを処理する単一のコールバックを各ストアregisterせます。 次に、ストアを変更するときはいつでも、イベントをdispatchします。

Reactのように、Dispatcherは、うまく実装された良いアイデアだと思います。 例として、ユーザーがやることリストに項目を追加できるようにするアプリには、次のものが含まれる場合があります。

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
 # in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
 # in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"

これにより、2つの質問に簡単に答えることができます。

  1. Q: MyStoreを変更するすべてのイベントは何ですか?
    • A: MyStore.dispatchCallbackswitchステートメントでケースを確認してください。
  2. Q:そのイベントのすべての可能なソースは何ですか?
    • A:そのactionTypeを検索するだけです。

これは、たとえば、 MyModel.set MyModel.save MyCollection.addなどを探すよりもはるかに簡単です。この場合、これらの基本的な質問への回答を追跡するのは非常に高速です。

Dispatcherでは、 waitForを使用して、コールバックを単純な同期方式で順次実行することもできます。 例えば:

 # in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore

実際には、 waitForを使用しなくても、Dispatcherを使用してストアを変更すると、コードがどれほどクリーンになるかを見てショックを受けました。

店舗

したがって、データはディスパッチャを介しストアに流れます。 とった。 しかし、データはストアからビュー(つまり、React)にどのように流れますか? Fluxのドキュメントに記載されているように:

[]ビューは、依存しているストアによってブロードキャストされるイベントをリッスンします。

いいでしょうストアにコールバックを登録したのと同じように、ビュー(Reactコンポーネント)にコールバックを登録します。 propsを介して渡されたストアで変更が発生するたびに再renderようにReactに指示します。

例えば:

 # in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item

素晴らしい!

では、どのようにしてその"change"イベントを発行するのでしょうか。 さて、FluxはEventEmitterの使用をお勧めします。 公式の例から:

 var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...

キモい! シンプルなストアが必要になるたびに、自分ですべてを作成する必要がありますか? 表示したい情報があるたびに使用することになっているのはどれですか? より良い方法が必要です!

行方不明の作品

バックボーンのモデルとコレクションには、FluxのEventEmitterベースのストアが行っているように見えるすべてのものがすでに含まれています。

生のEventEmitterを使用するように指示することにより、Fluxは、ストアを作成するたびに、Backboneのモデルとコレクションの50〜75%を再作成することをお勧めします。 店舗でEventEmitterを使用することは、Express.jsなどの十分に構築されたマイクロフレームワークがすでに存在し、すべての基本と定型文を処理する場合に、サーバーに裸のNode.jsを使用することに似ています。

Express.jsがNode.jsで構築されているように、BackboneのモデルとコレクションはEventEmitterで構築されています。 そして、それはあなたがほとんどいつも必要とするすべてのものを持っています:バックボーンはchangeイベントを発し、クエリメソッド、ゲッターとセッター、そしてすべてを持っています。 さらに、バックボーンのジェレミー・アシュケナスと彼の230人の貢献者の軍隊は、私ができると思われるよりも、これらすべてのことではるかに優れた仕事をしました。

このバックボーンチュートリアルの例として、MessageStoreの例を上からバックボーンバージョンに変換しました。

客観的にコードが少なく(作業を複製する必要がない)、主観的により明確で簡潔です(たとえば、 _messages[message.id] = messageの代わりにthis.add this.add(message) )。

それでは、バックボーンをストアに使用しましょう!

FluxBoneパターン:バックボーンによるフラックスストア

このチュートリアルは、BackboneforStoresを使用したFluxアーキテクチャであるFluxBoneと私が誇らしげに吹き替えたアプローチの基礎です。 FluxBoneアーキテクチャの基本的なパターンは次のとおりです。

  1. ストアは、ディスパッチャーにコールバックを登録した、インスタンス化されたバックボーンモデルまたはコレクションです。 通常、これはそれらがシングルトンであることを意味します。
  2. ビューコンポーネントがストアを直接変更することはありません(たとえば、 .set()はありません)。 代わりに、コンポーネントはアクションをディスパッチャにディスパッチします。
  3. コンポーネントのクエリストアを表示し、それらのイベントにバインドして更新をトリガーします。

このバックボーンチュートリアルは、Reactアプリケーションでバックボーンとフラックスがどのように連携するかを確認するように設計されています。

BackboneとFluxの例を使用して、その各部分を順番に見ていきましょう。

1.ストアは、ディスパッチャーにコールバックを登録した、インスタンス化されたバックボーンモデルまたはコレクションです。

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
 # in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore

2.コンポーネントがストアを直接変更することはありません(たとえば、 .set()はありません)。 代わりに、コンポーネントはアクションをディスパッチャにディスパッチします。

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent

3.コンポーネントは、ストアにクエリを実行し、それらのイベントにバインドして更新をトリガーします。

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent

私はこのフラックスとバックボーンのアプローチを自分のプロジェクトに適用しました。このパターンを使用するようにReactアプリケーションを再構築すると、ほとんどすべての醜い部分が消えました。 それは少し奇跡的でした。より良い方法を探して歯を食いしばったコードの断片が、賢明な流れに置き換えられました。 そして、Backboneがこのパターンに統合されているように見える滑らかさは注目に値します。単一のアプリケーションにそれらを合わせるために、Backbone、Flux、またはReactと戦っているような気がしません。

Mixinの例

FluxBoneストアをコンポーネントに追加するたびにthis.on(...)およびthis.off(...)コードを記述すると、少し古くなる可能性があります。

これは、React Mixinの例です。これは、非常にナイーブですが、確実にすばやく反復するのがさらに簡単になります。

 # in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
 # in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )

WebAPIとの同期

元のFluxダイアグラムでは、ActionCreatorのみを介してWeb APIと対話します。これは、ディスパッチャーにアクションを送信する前にサーバーからの応答を必要とします。 それは私には決して正しくありませんでした。 サーバーの前に、ストアが変更について最初に知っているべきではありませんか?

ダイアグラムのその部分を反転することを選択します。ストアは、Backboneのsync()を介してRESTfulCRUDAPIと直接対話します。 これは、少なくとも実際のRESTful CRUD APIを使用している場合は、非常に便利です。

データの整合性は問題なく維持されます。 新しいプロパティを.set()changeイベントがReactの再レンダリングをトリガーし、新しいデータを楽観的に表示します。 サーバーに.save()しようとすると、 requestイベントにより、読み込み中のアイコンが表示されることが通知されます。 処理が完了すると、 syncイベントで読み込みアイコンを削除するか、 errorイベントで赤に変更することが通知されます。 ここでインスピレーションを見ることができます。

また、防御の第1層の検証(および対応するinvalidイベント)と、サーバーから新しい情報を取得するための.fetch()メソッドもあります。

標準的でないタスクの場合、ActionCreatorsを介した対話の方が理にかなっている場合があります。 Facebookは「単なるCRUD」をあまり行っていないのではないかと思います。その場合、Facebookがストアを最優先しないのは当然のことです。

結論

Facebookのエンジニアリングチームは、Reactを使用してフロントエンドWebを前進させるために目覚ましい取り組みを行ってきました。また、Fluxの導入により、テクノロジーだけでなくエンジニアリングの面でも、真に拡張可能なより広範なアーキテクチャを垣間見ることができます。 Backboneを巧妙かつ注意深く使用すると(このチュートリアルの例による)、Fluxのギャップを埋めることができ、1人のインディーショップから大企業まで、誰でも印象的なアプリケーションを作成して維持することが驚くほど簡単になります。

関連: ReactコンポーネントがUIテストを容易にする方法