Managing Context In a Fluxible App Using React-Router

I recently began looking into using React Router with Yahoo's excellent Fluxible App framework and had a few issues with context so I thought I'd post what I found here to help anyone else that might be suffering in the same way.

Why change from Yahoo's own router implementation?

As a team we had been implementing our new site using the fluxible-plugin-router and while the routing had been fine for getting a basic prototype up and running, it was starting to get a bit messy and hacky as the site features became more concrete.

I've recently been looking at Ember and was really impressed with their router. Specifically, the ability to nest routes is a very powerful feature that I was missing using the Yahoo plugin.

Typically, our routes looked something like the following:

While this is a perfectly good way of managing routing in an app, I was interested in getting the benefits of nested views, transition hooks and the ability to abort, retry or redirect transitions that react-router offers. I was also keen to move the responsibility for data-requirements off the router and into the top-level handler components.

The only major concern was that fluxible-plugin-router was built from the start to support the requirements of a fully isomorphic application.

Switching to React-Router

The code below compares the two basic implementations of server-side rendering using the fluxible-plugin-router and react-router (examples of both can be found at https://github.com/yahoo/flux-examples/tree/fluxible-router)

On the face of things, it looks a simple switch out, however, there are a couple of import things to remember:

  1. context.createElement() is a little bit magic and manipulates the top-level component you provide when instantiating your app.
  2. In the react-router version, your top-level component is the router, not the component provided to the fluxible app

Working with React's context

Prior to React 0.13 context was created using the React.withContext() function, however, this has now been deprecated. Parent components should now declare the available properties using a childContextTypes object and getChildContext() function and child components declare the properties they want using a contextTypes object.

It's a powerful feature used by react-router and Fluxible to provide access to a number of helper functions. It means that any context made available by a component can be accessed by a distant relative at any depth in the component tree and it's fantastically useful (no more passing context down multiple layers of components via props!). Crucially, the provision of items via context is additive, so anything already in context remains available.

The problem

Jumping back to the basic implementation of server-side rendering with react-router above, there is a problem; non of the app's components have access to the FluxibleContext (more specifically the componentContext).

FluxibleContext is the glue that enables isolated handling of server requests and plays a key role in dehydrating and rehydrating the state of the app across the server-client connection. For components, the context should provide access to getStore() and executeAction() functions, but despite declaring them in contextTypes they remained undefined on each components this.context object.

If you remember from above, a typical Fluxible app has a root component defined as part of the app, but switching to react-router means the root component is the router itself. Browsing the source code we can see why the FluxibleContext functions aren't available - the router only declares routeDepth and router as childContextTypes.

As the Router is a React Component it is required to declare the various functions and properties that are available to its children via context but it isn't part of the Fluxible codebase so it doesn't know to provide the getStore() and executeAction() functions.

Fortunately, there is a simple fix to this problem (I say simple, it took me a good while to work this problem out and find the solutions). The key is to look inside the context.createElement() function provided by Fluxible.

Higher-Order Components

It's worth briefly touching on some other changes React 0.13 brought, most relevant to our problem being the support for ES6 classes and their lack of support for mixins.

Dan Abramov does a great job of discussing Higher-Order Components in his article Mixins Are Dead. Long Live Composition. In a nutshell, a Higher-Order Component is a function that provides additional functionality and/or data to a component and it's a technique that's used by FluxibleContext under the hood when you call context.createElement().

Mixins were (and still are if you use React.createClass()) a useful and powerful part of React and the Fluxible library relies on the FluxibleMixin to provide a lot of the functionality (often via context) to components in an app. However, without mixins we're forced to think again about how best to share functionality (or perhaps more accurately decorate components with new functionality).

One way could be to create a class (which extends React.Component) to inherit from but this is brittle and won't work in cases like ours where we are working with a third-party component.

Another way is to look at how this.props.children can be manipulated in a component's render function to provide extra functionality or data to child components. This is closer to the solution and gives a big clue as to how we can fix our context issue. Instead of looking to decorate children of the Router, we need to look at a way to make the Router the child of a component we have full control of.

Luckily, React gives us a perfect solution in the React.createElement() function. This function takes three arguments, type - the component class to use as the template for our Element, config - what we commonly know as props and children - the component tree to be rendered inside this node. Boom, this is the answer to our problem, we can create a new root element using React.createElement(), passing in the correct context as config and the Handler (aka the Router) as children.

FluxibleComponent vs provideContext()

The Fluxible module actually provides two different ways to work with Higher-Order Components, the FluxibleComponent component and provideContext() function.

FluxibleComponent is useful when you are in full control of the React view tree as you can set it as your root element and it will magically provide the relevant context.

provideContext() provides much the same functionality, except it also allows you to declare additional functions/properties to make available to children. It does this by returning a new wrapper component that accepts context as a prop and renders the provided component as its only child.

Making it all work

Finally, we're here! A quick recap.

  1. react-router provides the top-level component to render as the root of your DOM hierarchy
  2. The Router component doesn't accept context as a prop or declare the relevant FluxibleContext functions as being available to any child components
  3. We don't want to modify the source code for react-router to make it compatible with Fluxible

So, the solution ends up being quite elegant. Take the Handler component passed to the Router.run() callback, use provideContext() to wrap it with a Higher-Order Component that knows how to handle context passed as a prop (declaring thee getStore() and executeAction() functions in childContextType) and then use React.createElement() to create a new component instance to render to a string.

The final code is below:

Addendum

Michael Ridgway (@theridgway) pointed out to me that provideContext() will create an entirely new component on each request as it uses React.createClass().

While I like to use provideContext() as it allows extra items to be passed to children through the context, it is definitely better to use FluxibleComponent if you don't need that particular feature. The alternative code is shown below:

Ian Thomas

Father of two, Husband and UI Engineer, currently working at Sky Betting and Gaming

Leeds

Subscribe to Ian Thomas

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!