React and Flux
This article is not finished yet
and so contains notes and raw excerpts from other sources.
What is React
React is a project from Facebook that handles a very specific set of concerns. These are Packaging, composing and rendering HTML components. React is a very lightweight library that makes it easy to render complex UI’s by composing many simple components together. React utilizes a virtual DOM, which allows it to compare an old state of a page with the pages current state and compare the two in memory to attempt to update the DOM in the least expensive way possible. This pattern makes React very notably more efficient than many of the alternatives on the market today. React can also support rendering on client and server side which is critical for building isomorphic apps and really sets React apart from libraries like Angular or Knockout.
What is Flux?
Flux is the application architecture that FaceBook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow. It’s more of a pattern than a formal framework, and can be implemented immediately without a lot of new code in most situations.
Flux deliberately avoids using MVC in favour of unidirectional data flow. When a user interacts with a React View, the View propagates an action through a central dispatcher, to the various stores that hold the application’s data and business logic, which updates all of the views that are affected. This works especially well with React’s declarative programming style, which allows the store to send updates without specifying how to transition views between states.
Flux applications have three major components:
- The Dispatcher
- The Stores
- The Views
These components should not be confused with Model-View-Controller. Controllers do exist in a application but they are Controller-Views (Views often at the top of the heirarchy that retrieve data from the stores and pass data down to their children). Additionally, action creators – Dispatcher helper methods – are used to support a semantic API that describes all changes that are possible in the application. It can be useful to think of them as a fourth part of the Flux update cycle.
Control is inverted with stores: the stores accept updates and reconcile them as appropriate, rather than depending on something external to update its data in a consistent way. Nothing outside the store has any insight into how it manages the data for its domain, helping to keep a clear separation of concerns. Stores have no direct setter methods like “SetAsRead()”, but instead have only a single way of getting new data into their self-contained world – the callback they register with the dispatcher.
Structure and Data Flow
The Data flow of a Flux application is quite simple in that it flows in a single direction.
A unidirectional data flow is central to the Flux pattern, and the above diagram should be the primary mental model for the Flux programmer. The dispatcher, stores and views are independent nodes with distinct input and outputs. The actions are simple objects containing the new data and an identifying type property.
The views may cause a new action to be propogated through the system in response to use interactions:
All data flows through the dispatcher as a central hub. Actions are provided to the dispatcher in an action creator method, and most often originate from user interactions with the views. The dispatcher then invokes the callbacks that the stores have registered with it, dispatching actions to all stores. Within their registered callbacks, stores respond to whichever actions are relevant to the state they maintain. The stores then emis a change event to alert the controller-views that a change to the data layer has occurred. Controller-views listen for these events and retrieve data from the stores in an event handler. The controller-views call their own setState() method, causing a re-rendering of themselves and all of their descendants in the component tree.
This structure allows us to reason easily about our application in a way that is reminiscent of functional reactive programming, or more specifically data-flow programming or flow-based programming, where data flows through the application in a single direction – there are no two-way bindings. Application state is maintained only in the stores, allowing the different parts of the application to remain highly decoupled. Where dependencies do occur between stores, they are kept in a strict hierarchy, with synchronous updates managed by the dispatcher.
Two-way bindings tends to cause cascading updates, where changing one object leads to another object changing, which could also trigger more updates. As applications grow, these cascading updates make it very difficult to predict what could change as the result of one user interaction. When updates can only change data within a single round, the system as a whole becomes more predictable.
As mentioned previously, Flux applications have three major components:
- The Dispatcher
- The Stores
- The Views
A Single Dispatcher
The dispatcher is the central hub that manages all data flow in a Flux application. It is essentially a registry of callbacks into the stores and has no real intelligence on its own – it is a simple mechanism for distributing the actions to the stores. Each store registers itself and provides a callback. When an action creater provides the dispatcher with a new action, all stores in the application receive the action via the callbacks in the registry.
As an application grows, the dispatcher becomes more vital, as it can be used to manage dependencies between the stores by invoking the registered callbacks in a specific order. Stores can declarative wait for other stores to finish updating, and then update themselves accordingly.
Dispatchers should be loosely coupled and therefore reusable across multiple applications. As an example, the dispatcher that Facebook uses is available here:
Stores contain the application state and logic. Their role is somewhat similar to a model in the traditional MVC, but they manage the state of many objects – they do not represent a single record of data like ORM models do. Nor are they the same as Backbone’s collections. More than simply managing a collection of ORM-style objects, stores manage the application state for a particular domain within the application. A store exhibits characteristics of both a collection of models and a singleton model of a logical domain.
A store registers itself with the dispatcher and provides it with a callback. This callback receives the action as a parameter. Within the stores registered callback, a switch statement based on the action’s type is used to interpret the action and to provide the proper hooks into the store’s internal method. This allows an action to result in an update to the state of the store via the dispatcher. After the stores are updated, they broadcast an event declaring that their state has changed, so the views may query the new state and update themselves.
Views and Controller-Views
Close to the top of the nested view hierarchy, a special kind of view listens for events that are broadcast by the stores that it depends on. We call this a controller-view, as it provides the glue code to get the data from the stores and to pass this data down the chain of its descendants. There might be one of these controller-views governing any significant section of a page.
When it receives the event from the store, it first requests the new data it needs via the stores’ public getter methods. It then calls its own setState() or forceUpdate() methods causing its render() method and the render() of its descendants to run.
With this approach, it is possible to pass the entire state of the store own the chain of views in a single object, allowing different descendants to use what they need. In addition to keeping the controller-like behavior at the top of the hierarchy, and thus keeping the descendant views as functionally pure as possible, passing down the entire state of the store in a single object also has the effect of reducing the number of props that need to be managed.
Controller-Views that reside deeper in the hierarchy can violate the singular flow of data by introducing a new, potentially conflicting entry point for the data flow. Deep Controller-Views can help to simplify the applications logic, but can often complicate an application when performing data updates with the Render() method being invoked repeatedly by updates from different Controller-Views.
The dispatcher exposes a method that triggers a dispatch to the stores, and to include a payload of data which we call an Action. The Action’s creation may e wrapped into a semantic helper method which sends the action to the dispatcher. For example, we may want to change the text of a to-do list application. We would create an action witha function signature like UpdateText(todoId, newText) in our TodoActions module. This method may be invoked from within our views’ eent handlers, so we can call it in response to a user interaction. This action creator method also adds a type to the action, so that when the action is interpreted in the store it can respond appropriately. In our example, this type might be named something like TODO_UPDATE_TEXT,
Actions may also come from other places such as the server. This happens, for example, during data initialization. It may also happen when the server returns an error code or when the server has updates to provide to the application.