Relay and Routing

Relay is changing how we think about client side applications at Facebook. A large part of this is routing and its integration with our Hack/PHP stack. This post aims to explain why Relay in open source doesn’t have any routing features and explains solutions.


Note: This article was published in August 2015 and a lot has changed in the JavaScript ecosystem. This article is mostly outdated at this point.

One reason why React is so successful is because of its great incremental adoption story. I’ve recommended countless people to try out React by rewriting just one UI component of their app. Most of the time people love it and they’ll continue rewriting parts of their app using React until eventually the entire view layer uses React.

The adoption story is slightly different for Relay and the investment is far greater — you have to build a GraphQL server that delegates data fetching to databases and all client side data needs to be kept consistent with Relay’s store. However, what React is for views, Relay is for data and the incremental adoption story is still valid — it just requires a little bit more of an effort and quite often routing plays a big role. Joe Savona, a Relay core developer, best explained what Relay is in his Relay talk at React-Europe: UI = view(query(props)). This post explains how routing fits into declarative UIs.

Relay’s Core

Relay’s focus is data storage and keeping data in sync. Why would you ever think to include routing as part of data fetching? Relay actually started out as a routing and application lifecycle framework at Facebook before we brought React and GraphQL together. As we mapped out the open source roadmap for Relay, we realized that routing was not at the framework’s core any longer. We also took note of the amazing react-router library which is used by a lot of people in the React open source community. Rather than building a competing routing library we decided to strip all routing functionality from Relay. This gives developers the option to choose react-router, integrate Relay with their own routing solution or not use routing at all.

The only routing feature that Relay contains is Relay.Route. Internally, the Flow type for these is actually called RelayQueryConfig. While we use Relay.Route for routing at Facebook, Relay only requires that a route conforms to the shape of a RelayQueryConfig: {name, params, queries}. Instances of Relay.Route implement this shape to simplify the creation of a RelayRootContainer. This is what each field means:

  • name is a string that is required for caching together with params.
  • params are optional and are used to populate query variables with data such as story IDs, user names or any other token that represents an object in a graph.
  • queries are a description of your root fields — the entry points into your queries which compose fragments.

The way we have looked at queries and fragments in Relay is that routes and their GraphQL queries are absolute — they describe which particular object should be fetched — and containers and their GraphQL fragments are relative — they describe the shape of data required to render. A component and a route mapped together with a RelayRootContainer sufficiently describes the data requirements of an application.

using a RelayRootContainer to map a Component to a route.

react-relay ❤ react-router

Note: this is based on an upcoming 1.0 release of react-router. These APIs might change but I’ll do my best to keep this section up-to-date.

One goal of removing all routing features from Relay was to support react-router well. Incidentally both projects already used name and params in a similar way. The only thing that needs to be added to a react-router Route to make it work with Relay is queries.

Now we just need to connect these properties with a Relay container and pass them to a RelayRootContainer. I provided an RFC implementation for react-router for a previous iteration of their proposed 1.0 API. Ryan and Michael have since iterated and improved on their API. The new react-router API provides a top level createElement function on the router that intercepts when a route attempts to render its component.

Let’s take a look at a concrete example that works with the Relay starter kit. This brings together all the concepts explained above and shows how they work together.

an app using Relay and react-router together.

The full repository and code can be found here. The important pieces are the createRelayContainer function and how it is being passed to the Router on line #50. With this solution, instead of using Relay.Route we are using react-router exclusively to construct our RelayQueryConfig and pass it along with a Component to a RelayRootContainer. In fact, createRelayContainer is just a higher order function, very similar to a higher order component (HoC) in React. We use the params from react-router and pass them directly into the query function: take a look at how the /widget/:id is defined as the path and how $id is used in the widget query.

This solution will work for most smaller applications but it has one flaw: on every level of the nested route tree, when it finds a Relay component, it will render a new RelayRootContainer. This means that on every step of the route tree, Relay has to fetch all the data for that level until it can proceed to fetch data for the next level in the route tree. Relay is trying to solve the multiple-API-request problem by using GraphQL but if you are using the above solution you will not benefit from Relay’s optimizations.

Internally we discussed building a RelayAggregateContainer — in fact it was kind of a stretch goal for my work on routing but as I moved on to work on the newly (re)formed JS Infra team at Facebook I figured I’ll let open source take care of it. And sure enough, less than a week after Relay was open sourced, Gerald Monaco and Jimmy Jia built react-router-relay. It supports an arbitrary level of nested Relay containers and root queries. Here is what it looks like:

That’s it! It is very similar to the example before but we are hiding all the magic in the react-router-relay module and we gain the ability to fetch data for nested routes all at the same time. Here is the full file. Smiles all around!

Relay’s Routing at Facebook

Now how does routing at Facebook work? We use Relay.Routes throughout all Relay apps. Since the routing system is not tied to Relay any longer, we are now also using our routing system in regular React applications and it provides a nice incremental upgrade path towards Relay.

routing at Facebook is almost like this.

Because all of our routing is done in Hack/PHP, every route defined in JavaScript gets compiled to a Hack/PHP route-map via static-analysis. We also have a class called RelayRouter on the client which matches a URI against all the registered routes and returns a Relay.Route instance that can be passed on to a RelayRootContainer. Of course, product developers don’t have to worry about any of this— they simply need to define their routes and the components that map to them. We link between routes using a URI builder like this:

a link to a profile page.

Of course we need a way of branching based on routes: developers need to be able to tell which components render for which route. For this we use a small helper called matchRoute which does a simple pattern matching on the routeName.

the entire implementation of matchRoute

We call matchRoute in our render methods of React components. This means that we embed the route-tree inside the view layer. There is no 1-to-1 mapping between a route and a component, they don’t even know about each other. We made it so our JavaScript dependency system understands matchRoute calls and only conditionally downloads and executes the code that is part of its callback functions. It is unfeasible to download all JavaScript of an entire client-side application upfront. With matchRoute our product engineers have a simple tool to require JavaScript code based on the current view. The Relay routing and boot-loading system makes sure that all the dependencies are downloaded before we re-render the application on a route transition.

Unlike react-router we do not support nested routes but we are currently experimenting with adding a matches field to routes that allows developers to express that a route also matches the matchRoute call of a parent route. As an example, a ProfilePhotoRoute would also match a ProfileRoute. This requires our build system to learn about route dependencies.

Conclusions

Relay’s routing at Facebook is powerful but very specific to our stack. We built framework that covers the entire client side that also integrates with our backend. We believe Relay should be a data-only framework and we are more interested in supporting solutions that the open source community will come up with for routing. We have no doubt that we will also benefit and be able to improve our internal implementation of routing at Facebook with your help.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.