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:
context.createElement()
is a little bit magic and manipulates the top-level component you provide when instantiating your app.- 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 thegetStore()
andexecuteAction()
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.
- react-router provides the top-level component to render as the root of your DOM hierarchy
- The
Router
component doesn't accept context as a prop or declare the relevantFluxibleContext
functions as being available to any child components - 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()
.
@anatomic As a note, it might be more efficient to use FluxibleComponent with react-router because provideContext creates a new component.
— Michael Ridgway (@theridgway) April 15, 2015
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: