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 hisRelay talk at React-Europe:UI = view(query(props)). This post explains how routing fits into declarative UIs.
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 amazingreact-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 isRelay.Route. Internally, theFlow type for these is actually calledRelayQueryConfig. While we useRelay.Route for routing at Facebook, Relay only requires that a route conforms to the shape of aRelayQueryConfig: {name, params, queries}. Instances ofRelay.Route implement this shape to simplify the creation of aRelayRootContainer.This is what each field means:
The way we have looked at queries and fragments in Relay is thatroutes and their GraphQL queries are absolute — they describe which particular object should be fetched — andcontainers and their GraphQL fragments are relative — they describe the shape of data required to render. A component and a route mapped together with aRelayRootContainer sufficiently describes the data requirements of an application.
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 usedname andparamsin a similar way. The only thing that needs to be added to a react-routerRoute to make it work with Relay isqueries.
Now we just need to connect these properties with a Relay container and pass them to aRelayRootContainer. I provided anRFC implementation for react-router for a previous iteration of their proposed 1.0 API.Ryan andMichael have since iterated and improved on their API. The new react-router API provides a top levelcreateElement 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 theRelay starter kit. This brings together all the concepts explained above and shows how they work together.
The full repository and code can be foundhere. The important pieces are thecreateRelayContainer function and how it is being passed to theRouter on line#50. With this solution, instead of usingRelay.Route we are using react-router exclusively to construct ourRelayQueryConfig and pass it along with a Component to aRelayRootContainer. 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 aRelayAggregateContainer — 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 andJimmy Jia builtreact-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 thereact-router-relay module and we gain the ability to fetch data for nested routes all at the same time. Here is thefull file. Smiles all around!
Now how does routing at Facebook work? We useRelay.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.
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 calledRelayRouter on the client which matches a URI against all the registered routes and returns aRelay.Route instance that can be passed on to aRelayRootContainer. 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:
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 calledmatchRoutewhich does a simple pattern matching on therouteName.
We callmatchRoute in ourrender methods of React components. This means that weembed 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 understandsmatchRoute 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. WithmatchRoute 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 amatches field to routes that allows developers to express that a route also matches thematchRoute call of a parent route. As an example, aProfilePhotoRoute would also match aProfileRoute. This requires our build system to learn about route dependencies.
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.
Formerly Pojer · 👨🏻💻 Engineering Manager at Facebook · 🃏 Jest · 📦 Yarn · 🚇 Metro