Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@appsforartists
Last active August 29, 2015 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save appsforartists/3fbb573ee46239c42ed5 to your computer and use it in GitHub Desktop.
Save appsforartists/3fbb573ee46239c42ed5 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@appsforartists
Copy link
Author

Composable Flux:

Using the concepts of functionally-reactive programming to make Flux ambidextrous

Collections + Selectors

As an industry, we've made digital incarnations of newspapers, shopping catalogs, mailboxes, image galleries, and many other artifacts of the urban world. Although in meatspace these are all very distinct concepts from one another, their digital counterparts all embody a single paradigm: show the user a list of {articles, products, messages, images} and allow the user to choose one to see in more detail.

You can model this relationship as the intersection of a collection and a selector. The collection represents all the known objects and the selector tells you which one(s) you're interested in:

products   current product ID
        \ /
         V
    the marlin

The selector might be determined by the URL:

/products/`:currentProductID`/

Or, it can even come from another collection:

      products   productIDs in category #5
              \ /
               V
    products in bike category
               |
          take first 10
               |
products on page 1 of bike category

Combining data sources to make new data sources is the hallmark of a technique known as functionally-reactive programming. Here's what the first example might look like with FRP:

"currentProduct": (products, currentProductID) => products.get(currentProductID)

If either products or currentProductID changes, that function will be re-run. If its result has changed, any other stores or components that depend on it will receive the new value of currentProduct. They, too, will update, propagating their changes throughout the system.

By applying this technique to the Flux architecture, we can make our codebases both simpler and more expressive, while also making them more easily isomorphic.

Advantages to This Approach

  1. It can make your site feel instantaneous.

On the legacy web, when you search for a product, open your inbox, or go to the home page of your favorite news source, you're presented with a list of options. A typical product listing includes the product's name, price, photo, rating, and manufacturer. When you chose one, you'll be taken to a details page that prominently features each of those bits. Even though you already have most of the information you need to render the details page, the server has no way to take advantage of this. Before you can see the details page, you'll have to wait while the server looks it all up again.

With a well-designed architecture, we can completely eliminate this wait: the details page will render immediately with the data it already knows while it progressively loads the rest. Combine this with some animated sleight-of-hand and a reasonably fast data source, and the user won't even notice the bits that loaded progressively: your site will feel instantaneous.
2. It's crawlable.

The simplest way to achieve instant rendering is with a so-called Single Page Application. In this model, instead of serving an HTML page, the server returns a bunch of JavaScript that creates pages dynamically using DOM manipulation and the History API. When all your clients are modern web browsers that speak JavaScript and understand the DOM, Single Page Applications suffice. But, sites on the public web shouldn't make that assumption, because we have an ecosystem of search engines and archival tools crawling the web, and they often won't know what to make of a giant ball of JavaScript.

The architecture that enables all that instant-render goodness without sacrificing SEO is called isomorphic. It renders the first page on the server and subsequent pages on the client. To do so, the server needs to know precisely what data to load and to detect when that data's been loaded before it can render. Unfortunately, most Flux architectures don't start retrieving data until after the first render. When data retrieval is coupled to rendering, isomorphism is impossible.

By making Flux functionally-reactive, the solution becomes really simple: make a readyToRender store that composes the others:

"readyToRender":  (routeName, currentProduct, currentCategory) => {
                    switch (routeName) {
                      case "productCatalog":
                        return currentCategory !== null;

                      case "productDetails":
                        return currentProduct !== null;
                    }
                  }

Conceptually, readyToRender is just another derived data source; it looks at the URL and the current state of the stores and returns true when all the data that's needed to render that URL has been fetched. With a little sugar to make it more concise and declarative, it's a really nice way to solve the isomorphic problem.
3. It's super expressive.

In the days before Flux, declaring a variable was a one-line endeavor:

var currentProduct = products.get(currentProductID);

Other Flux implementations add a bunch of boilerplate, but functionally-reactive Flux is just as simple:

"currentProduct": (products, currentProductID) => products.get(currentProductID)
  1. It can minimize data transfer.

To make isomorphism effective, the server needs to marshall the data it loaded to the client. As you might imagine, serializing all the stores and sending them alongside the rendered HTML can add significant weight to your responses.

In a functionally-reactive architecture, it's easy to tell what data came from the network and what can be derived internally. By serializing only the external data, we minimize our impact on the response size; everything else is automatically recreated on the client.
5. It can support some fantastic tooling.

One of the great things about functionally-reactive programming is that it's declarative - your inputs and outputs are all clearly defined in a way that's easy for tools to parse.

Because data isn't always coupled to UI, debugging is traditionally a tedious process that relies on breakpoints and log statements; finding the precise point where things started to go awry is a lot like finding a needle in a proverbial haystack. With a functionally reactive Flux architecture, we can add a tab to the Dev Tools that automatically visualizes your data as it flows through the system. When you can actually see what you're doing, mistakes are a lot easier to find.

Overview of Existing Methodologies

Let's see how these concepts work in two of the leading Flux implementations, Facebook Flux and Reflux.

Facebook Flux

For as many words have been written about Flux, it's much easier to understand with a concrete example. For that, we turn to Ian Obermiller's NuclearMail. NuclearMail is an excellently-written sample application that demonstrates the techniques Ian's team used to port the Atlas ad server's web interface to React and Flux.

In NuclearMail, data is loaded by the DependentStateMixin: When a component is rendered, the DependentStateMixin uses its component's state and props to fetch data from the appropriate store. If the store can't fulfill the request, it tells the API adaptor to go fetch that data. When the API receives data, it publishes a message on the Dispatcher and tells the store. The store updates itself, which tells the DependentStateMixin to update the component's state.

In this implementation, the DependentStateMixin serves the same purpose as the selector in our proposal, but with a crucial difference: it lives in the component, outside the purview of the Flux architecture. This causes a few problems:

  1. There's no way to programmatically determine if the data is ready to render, because it's tightly coupled to the rendering; therefore, an isomorphic architecture is much harder to achieve.
  2. It's not composable. The DependentStateMixin presumes that your data source is in an external store, but dependent state doesn't exist externally. Composability is traditionally one of React's strengths. Derived data should be able to depend on other derived data just as easily as it depends on external sources.

The DependentStateMixin is an elegant solution to derived data for Facebook Flux, but as these limitations show, there's still room for improvement.

Reflux

Reflux is the most popular alternative to Facebook Flux. It's also a bit simpler. In Reflux, stores listen to actions directly, with no dispatcher to intermediate. Stores can also listen to other stores. In this way, they are more composable than their Flux counterparts. For instance, here's the Reflux version of the currentProduct example:

var currentProduct = Reflux.createStore(
  {
    "listenables":        {
                            "ProductsUpdated":   stores.Products,
                            "ProductIDUpdated":  actions.viewProduct,
                          }

    "onProductsUpdated":  function (products) {
                            this.products = products;

                            tryToUpdate();
                          },

    "onProductIDUpdated": function (productID) {
                            this.productID = productID;

                            tryToUpdate();
                          },

    "tryToUpdate":        function () {
                            var oldState = this.state;
                            var newState = this.products.get(this.productID);

                            if (newState !== oldState) {
                              this.state = newState;
                              this.trigger(newState);
                            }
                          }                              
  }
)

That's an awful lot more boilerplate than:

"currentProduct": (products, productID) => products.get(productID)

We had to manually keep track of this.products, this.productID, and this.state; we also manually called tryToUpdate() every time a dependency changed. Bugs happen when you overlook those little details.

Finally, Reflux doesn't enforce any constraints around what you trigger. When you call this.trigger(newState), it ought to automatically store that value as this.state. Not only would that eliminate some of the boilerplate, but it would also give you a consistent place to look up the store's last value. This is important for isomorphism - when it's time to transition from the server to the client, serialize each store's state and deserialize them on the client.

These are not insurmountable problems - they could be fixed in a future version of Reflux, or with a wrapper library. But if we're going to take the time to create a wrapper library, maybe we should be teaching an FRP library about Flux rather than teaching a Flux library about FRP. That way, we'd get map, filter, combine, and all the other functional operations for free.

Relay and Falcor are coming - why bother reinventing Flux?

Polishing Flux at this point might sound like building a better buggy whip in 1909. Facebook has publicly teased Relay as the successor to Flux. Netflix will be open-sourcing its data layer Falcor imminently. Why improve Flux now when these are "coming soon"?

Both Relay and Falcor expect to be paired with a server that implements its own corresponding API. For the foreseeable future, many web applications will need to be able to consume data from an arbitrary API. So long as that's true, there's room for a better Flux to coexist with Relay and Falcor.

How do we make this A Thing?

As I've hopefully articulated, there are some clear advantages to be achieved by bringing together functionally-reactive programming and Flux. However, this is not an announcement - it's merely a proposal. There are still plenty of open questions to be answered.

If this project sounds interesting to you:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment