Upgrading to Relay Modern or Apollo

Scott Taylor
17 min readAug 3, 2017

--

I wrote previously about how the New York Times is basing its re-platform/redesign on React and GraphQL, and I singled out Relay as our choice of framework for connecting the two.

All of those things remain true, but in an ecosystem that moves extremely fast (NPM/Node) and creates epic amounts of vaporware and abandonware, we must constantly reevaluate all of our decisions and assumptions to make sure we are building a solid foundation for the future.

Relay has now been dubbed Relay Classic, and the new cool kid is Relay Modern. Migrating from Classic to Modern is not a walk in the park, which I’ll talk about below. We need to know where these platforms are heading, and we thought it would be a good time to compare Relay Modern and Apollo to weigh our options for the future.

Disclaimer

This post is not going to prefer one project or the other, and it is not going to foreshadow any decision on our side. We are in no danger of moving too fast in either direction. In fact, Relay Modern has moved closer to Apollo in a lot of ways, so any pre-requisite work we do to transition our Classic codebase will move us closer to both of them.

Assumptions

  • GraphQL is the future
  • We are going to use React to build our UIs
  • The app we build has to be universal/isomorphic:
    fully-rendered on the server and the client
  • The app needs capabilities like client-only queries to allow the server response to be cached in a CDN (server-rendering in React is …. slow) and not rely on cookies

Proof of Concept

The NYT contains a lot of components, all with GraphQL fragments of varying degrees of complexity. Using the NYT codebase to try-out a new framework is not always practical. As such, I wrote an end-to-end product that is much simpler. It is also an open source project I am working on: a headless WordPress “theme” built on GraphQL, with versions for Relay, Apollo, and an app written in React Native. The motivation: I want to use WordPress as my CMS, but I do not like writing UIs in PHP. I do like writing UIs in React, and I think Relay and Apollo are cool.

The GraphQL server reads its data from the WordPress REST API and exposes a “product schema” that describes the data a WordPress theme probably needs to build a site that has parity with a theme written in PHP using WordPress idioms. The schema could actually be resolved by a backend that is not WordPress, which is the beauty of GraphQL: I describe my product, the data resolution and implementation details are opaque.

The WordPress Rest API does not expose enough data by default to build a full theme, so I extended the API with my own endpoints. They are enabled by activating my WordPress GraphQL Middleware plugin in a WordPress install. The plugin is only available on GitHub right now, as it is still in active development.

The GraphQL server is mostly stable. It uses the reference implementation of GraphQL from Facebook, graphql-js. I also leaned on Jest for unit-testing, and DataLoader (a game-changer) for orchestrating batch requests to the REST API.

The first implementation I did was on Relay Modern: relay-wordpress. For our recent Maker Week (work on whatever you want) at the Times, I wrote a React Native version of the same app.

To play devil’s advocate, I also tried out the same app in Apollo: apollo-wordpress. I went in with a bias towards Relay and came away with a completely different perspective.

It is possible that neither is the right framework, or they are both equally as good. I think it all depends on your evaluation criteria. I will evaluate both of them below. My notes on Relay Classic are based on work I have done at the Times. The examples in Relay Modern and Apollo are drawn from my open source projects.

Picking a Router

When using Relay with a Node server like Express, you can typically pipe all routes to the same resolver and have your app’s internal router control the render path. The router might also take props that allow you to specify GraphQL queries associated with the matched path. Example:

// routes/index.js<Route component={App}>
<Route path="/:id" component={Story} queries={{
story: () => Relay.QL`query { article(id: $id) }`,
}} />
<Route path="/" component={Home} queries={{
home: () => Relay.QL`query { home }`,
}} />
</Route>

It is essential that the router consumes a static route config that can be read in a repeatable and predictable way. Without knowing the route config, it is not currently possible to extract all queries for a particular route to:

1) request data on the server
2) rehydrate the client with the same data

This avoids making GraphQL queries on the server and then immediately requesting the data again when the page loads on the client.

Relay Classic:
We need:

On the client, <Router> from IRRR wraps <Route>s from RRv3. Because we are using IRRR, we know how to read the props on <Route> that specify the Relay queries associated with a given path. IRR supports rehydration, but we had some problems with it (probably self-inflicted).

Relay Modern
We use Found Relay — there is currently no alternative when you need isomorphic rendering. Found Relay is the <Router> and Route. There is no mix and match. Found Relay has a naive approach to client rehydration that is less than ideal.

IRRR and IRR have not been updated to work with Relay Modern, and there appear to be no plans for them to do so. Because those libraries are pre-reqs for RRv3 to work with Relay, RRv3 is not an option. React Router v4 does not have any knowledge of Relay, it does not support middleware (RRv3 does), and <Route>s can be rendered anywhere in the component tree. Even if we could do a render pass to extract all of the possible queries from the routes, they still might not be comprehendible — it is entirely possible that nested component query variables are constructed dynamically at runtime.

Apollo
We would use… any router we want. Queries are not configured on routes. Apollo out-of-the-box does a render pass that can comprehend possible queries. Rehydration works out of the box. Apollo uses Redux under the hood, so this whole process is pretty elegant.

Takeaways: Apollo takes away a lot of drama here. Found Relay works, but does not have a huge community behind it. The maintainer, Jimmy Jia, has been really helpful though and is always open to talk about these technical challenges. React Router v4 is possibly an anti-pattern for isomorphic apps, although it works just fine on React Native.

Environment

The “environment” is the layer that actually encapsulates network fetching, the store, and caching.

Relay Classic
There is a Relay.DefaultNetworkLayer that can be configured to talk to GraphQL using fbjs/lib/fetch, or you can roll your own (this part is strange in Classic). This initialization only happens on the client, the server uses IRRR’s imperative API.

Relay Modern
You create an instance of Environment that contains instances of other pieces, including an instance of Network, which you pass your Fetch implementation to. The fetch() implementation can be really simple, basically the version you get from “Hello, World” will work for querying your GraphQL server. relay-runtime also exposes a QueryResponseCache object that accepts TTLs that you can use in your fetch implementation. The caching is nice for React Native apps, where constant data-fetching is less necessary. Invalidation can become too complicated otherwise, unless you are just using low TTLs for performance. Persisted queries in Relay are currently a roll-your-own-imlementation (I did it here). The logic to send an ID instead of the query text lives in the fetcher.

Example using Found Relay and its Resolver:

export function createResolver(fetcher) {
const environment = new Environment({
network: Network.create((...args) => fetcher.fetch(...args)),
store: new Store(new RecordSource()),
});
return new Resolver(environment);
}

Example using React Native:

const source = new RecordSource();
const store = new Store(source);
const network = Network.create(fetchQuery);const environment = new Environment({
network,
store,
});
export default environment;

Apollo
This logic is tucked away in ApolloClient on the server and the client. ApolloClient is also where persisted queries can be configured. Apollo doesn’t need your GraphQL schema, but this is also where you specify the JSON config for your fragmentMatcher, which lists all of your Union and Interface types. Without this, Apollo will throw a bunch of warnings that it is using heuristics to determine types at runtime. Example:

// server
const client = new ApolloClient({
ssrMode: true,
networkInterface: new PersistedQueryNetworkInterface({
queryMap,
uri,
}),
fragmentMatcher,
});
// client
const client = new ApolloClient({
initialState: window.__APOLLO_STATE__,
networkInterface: new PersistedQueryNetworkInterface({
queryMap,
uri,
}),
fragmentMatcher,
});

Takeaways: Apollo has an elegant solution. Relay Modern works out of the box with React Native, needs some 3rd-party love on the web.

Fragments

GraphQL fragments are typically co-located with React components. Higher-Order Components (HOCs) (via ES7 decorators or an imperative API) glue the components together with their “fragment container.”

Relay Classic
Fragments are atomic units. They are lazily constructed via a thunk that returns the result of the Relay.QL tagged template literal. Unless I am mistaken, the Relay Classic Babel plugin intercedes here and turns the result into an AST at runtime, a GraphQLTaggedNode. Nested Components fragments can be included via Component.getFragment(‘node’) calls, and are subject to Data Masking: which means Relay will hide the portion of the query from all components except the one that required the specific slice of data represented by the getFragment call. Here’s a concrete example:

fragments: {
card: () => Relay.QL`
fragment Card_card on Asset {
__typename
... on CardInterface {
card_type
promo_media_emphasis
news_status
promotional_media {
__typename
... on AssetInterface {
promotional_media {
__typename
}
}
}
}
... on AssetInterface {
last_modified_timestamp
last_major_modification_timestamp
}
${CardMeta.getFragment('card')}
${HeadlineCardContent.getFragment('card')}
${VideoHeadlineCardContent.getFragment('card')}
}
`,
},

WARNING: these fragments, in addition to allowing the inclusion of other component’s fragments (by design), also allow interpolation of string literals, and can be informed by variables local to the HOC itself via initialVariables and prepareParams, props that also lived on Relay.Route, back when Relay was a routing framework at Facebook.

Because these fragments can take local variables, the result of this fragment can not be known statically, as prepareParams can construct literally whatever it wants. Even more dangerous: the inclusion of fragments from another container can also be informed by these same variables. Example:

{
initialVariables: {
crop: ‘jumbo’,
},
prepareParams: () => { // set crop based on runtime },
fragments: {
media: ({ crop }) => Relay.QL`
fragment on Media {
${NestedComponent.getFragment(‘media’, { crop })}
}
`,
},
}

These types of fragments cannot be statically analyzed, and cause cascading complexity as they are passed down.

Relay Modern
The tagged template literal around fragments is graphql. Fragments are strings with no interpolation. ${Component.getFragment(‘node’)} becomes, simply, ...Component_node. The actual component whose fragments you are spreading does even need to be in scope, so it is possible you can import fewer modules. A caveat: all fragments now need to be named.
fragment on Media { ... } needs to be:
fragment Component_media on Media { ... }.

The naming convention is not arbitrary. It is: {FileName}_{prop}. If you use index.js as the name of your file, the name will be the name of the parent folder.

Why do all of this?
All fragments and queries are known at build time this way, and can be statically analyzed when your app builds, so no runtime parsing has to take place.

Relay Modern has a compiler called relay-compiler that introduces a build step to your app. The build step generates artifacts. The Babel plugin for Relay Modern (not the same plugin as Classic!) causes your graphql tagged template calls to lazy-require the artifact files that look like:
Component/__generated__/Component_media.graphql.js.

Apollo
The tagged template literal for fragments is gql. The default pattern for specifying fragments for a component is via a static member on the component class called fragments. Apollo uses the ...Component_media syntax for spreading fragments of other components, but it also requires you to add the actual fragment to the bottom via string interpolation. Example:

fragment Component_media on Media {
id
...Image_image
}
${Image.fragments.image}

Apollo does not enforce Data Masking, and doesn’t require co-locating your fragments, so fragment “snippets” (an anti-pattern and probably dangerous as pertains to compatibility) are more share-able by default. You can actually place the fragments in their own .graphql files and use the graphl-tag module to load them. Instead of including the fragments from other components, you use the #import syntax exposed by the Webpack loader:

// Component_media.graphql#import "./Image_image.graphql"fragment Component_media on Media {
id
...Image_image
}

An advantage to this approach is syntax-highlighting in your editor.

Takeaways: Fragments in Relay Modern are much cleaner and enable static queries. The ergonomics of fragments in Apollo are different, and possibly better when fragments are placed in separate files. However, this breaks the idea of co-locating fragments with React components. So, this may be a religious debate. What is most obvious to me: dynamic fragments in Relay Classic have to go.

Queries

Relay Classic
The queries for a given path live in the route config, and all of the fragments in the component tree below specify which parts they are interested in. A implicit component hierarchy is necessary, and is strictly enforced by Data Masking. You never specify the whole query in one place, you simply say: query { asset(id: $id) } and the rest is mostly magic.

Relay Modern
Since Found Relay is our only option for routing right now, you specify the query fragment on the route, via a graphql tagged template, or by including the module that exposes the query. I actually suggest that you place all top-level queries in a folder called queries. The query can be statically analyzed at build time:

// queries/Story_Query.jsimport { graphql } from 'react-relay';export default graphql`
query Story_Query($id: ID!) {
...Story_whatever
}
`;

Data Masking is still enforced, but this query is represented in the AST as an entire query. The build artifact exports what is called a ConcreteBatch, which contains a node called text, which contains the plaintext GraphQL query. Because the text for the query is known, and the process for retrieving it is nominal, an ID can be assigned to the text representation, and sent in place of the query, so long as the GraphQL server knows this is happening and is able to turn the ID into the full query text.

Both servers, Relay and GraphQL, have to be set up to comprehend this exchange.

Instead of your fragments containing dynamic fragments, your queries take top-level variables. This may require rethinking portions of your app that wanted to remain dynamic at runtime. You may also have to request more data than you did in Classic and filter data at runtime.

In our Classic example above that had a fragment variable called crop, we need to transition that fragment to receive a variable from the query itself. Here’s what it might look like after transition:

graphql`
fragment Component_node on Media {
crop(size: $crop) {
url
width
height
}
}
`

It might be hard or weird to transition some of your components in this way, but the payoff is use of a leaner Relay core and no expensive runtime query parsing.

Relay Modern exposes a Component called QueryRenderer. <QueryRenderer> can be dropped anywhere in your component tree and given your Relay Environment instance, a query, and variables, make a request to your GraphQL server that is passed to a function exposed on the render prop. From the Relay Modern docs:

import { QueryRenderer, graphql } from 'react-relay';

// Render this somewhere with React:
<QueryRenderer
environment={environment}
query={graphql`
query ExampleQuery($pageID: ID!) {
page(id: $pageID) {
name
}
}
`}
variables={{
pageID: '110798995619330',
}}
render={({error, props}) => {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return <div>{props.page.name} is great!</div>;
}
return <div>Loading</div>;
}}
/>

In React Native, this is great, and makes your choice of routing solution less critical. There is no server/client scenario in native apps, which is why Relay Modern “just works” there.

On the web, and specifically for universal rendering, this is a problem. Because these queries can live somewhere other than your route config, it can become impossible to know all of your queries ahead of time to request data properly for server rendering your app. This is the task IRR did in Classic. As such, QueryRenderer can only be used for client-only queries.

Client-only queries are not a first-class citizen in universal Relay apps, so QueryRenderer should NOT be used for queries tied to routes, Found Relay will handle those queries. QueryRenderer is only to be used for “extra” data that might be a result of a user interaction or page scroll. Found Relay attempts to implement the insides of QueryRenderer when it resolves data on the server.

Apollo
Queries can live anywhere via the graphql HOC, typically used as an ES7 decorator. On the server, Apollo does an initial render pass that only extracts data. Example:

import { graphql } from 'react-apollo';
import PageQuery from 'graphql/Page_Query.graphql';
@graphql(PageQuery, {
options: ({ params: { slug } }) => ({
variables: {
slug,
},
}),
})
export default class Page extends Component { ... }

Takeaways: all three solutions query data in wildly different ways. Something to note: the trend is towards one Query per route. This can easily be accomplished if you make a top-level GraphQL type called Viewer and specify all possible queries as fields below it.

Mutations

Relay Classic
Mutations are constructed via configuration, some of which uses Flux idioms. They are confusing and weird. The “store” is updated via a Flux config. The whole process is very black hole. Rather than dive in here, just peruse the docs.

Relay Modern
Mutations use an imperative API that is very similar to Apollo’s. Updating the store is mostly a manual process that requires interacting with the ConnectionHandler API. Mutations still require a config object, but one that is (slightly) less confusing. Optimistic updates use the same data you passed to create the mutation. Mutations require your instance of Environment. The documentation for interacting with the store is basically non-existent. Changes to the store cause UI to re-render. Editor’s Note: I just checked the docs, and they have now included references in Relay Modern to the Flux-style configs. Good Luck. I’m not even sure that looking at code makes this easier to understand, but here are some mutations I added to perform CRUD on a post’s comments.

Apollo
Uses an imperative API. Interacting with the store is kinda strange — this is not a knock. Changes are made manually and then committed back. Although they claim that data is more consistent in their store, I can see how developer error can mess this up. Mutations are specified using the graphql HOC (like queries), and I like how this encourages the creation of atomic components to handle each mutation. For instance, rather than having a button that calls a method on its parent component triggering a mutation on click, the button itself can become a component that is wrapped in a HOC. The HOC provides the wrapped component with a mutate prop. Here is my Delete Comment button.

Takeaways: Relay Modern moves closer to Apollo, but seems to not be able to quit the confusing Flux configs. Apollo has a pretty nice solution but a different Store implementation. Reading and writing are really different across Relay and Apollo. Optimistic UI updates are pretty similar. I also like how Apollo allows you to specify refetchQueries when calling a mutation. That way, your mutation can return a small amount of data needed for the UI update, and the refetch queries can keep your store up to date.

Refetching

There may be times where you want to “refetch” a route’s top-level query with new query variables. For me, the most obvious reason is a feature like Search. In a single page app, I want to do as much as possible without a full page reload. Refetch behavior makes this possible.

Relay Classic
If you want to “refetch” the query on the client based on new input or client-only flags being set, you call this.props.relay.setVariables({ ... }) in a component. This will either affect the variables of the fragment they are called in, or cascade down the chain. setVariables() also triggers a store lookup and is when the cryptic “node” query is triggered. I am actually not sure if calling this function in a nested component can affect ancestor component variables, and I really don’t want to try to find out.

Relay Modern
There are different types of “containers” — in Classic, Relay.createContainer() is the HOC. In Modern, Fragment Containers are created with createFragmentContainer(), and Refetch Containers are created via createRefetchContainer(). Creating a Refetch Container enables the following method: this.props.relay.refetch({ ... }). When creating the Refetch Container, you specify what query you are refetching. Example, I created my own decorator to call createRefetchContainer:

// decorators/RefetchContainer.jsimport { createRefetchContainer } from 'react-relay'; export default (spec, refetch) => component =>   
createRefetchContainer(component, spec, refetch);
// routes/Search.js@RefetchContainer(graphql`
fragment Search_viewer on Viewer {
posts(search: $search, first: $count) {
edges {
node {
...Post_post
}
cursor
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
`,
SearchQuery)
export default class Search extends Component { ... }

I do not remember what happens if you don’t specify the query. Maybe it refetches the same query? The Relay docs market this HOC as useful for “Load More”, but as you’ll see below, there is also a Pagination Container, which is also concerned with Loading More. I prefer to think of the Refetch Container as a place to perform inline updates, or to refetch the query with client-only data.

Apollo
You always have access to a prop called data in wrapped components, and data has a method called refetch() that works like Relay Modern. Example:

doRefetch = debounce(() => {
this.props.data.refetch({
...this.props.data.variables,
search: this.state.term,
}).catch(e => {
if (e) {
console.log(e);
}
}).then(() => {
this.input.blur();
});
}, 600);

Takeaways: Apollo doesn’t require doing anything new or different. The Relay Modern approach is useful and only requires a small amount of configuration. Relay Modern highlights the need to have your queries in a separate file so you DRY, as they may be needed in multiple places.

Pagination Containers

Most apps, especially something like a blog, need pagination and/or Infinite Scroll. All implementations make this easy.

Relay Classic
Pagination is mostly a roll-your-own solution using something like: this.props.relay.setVariables({ count: prevCount + 10 })

Relay Modern
You use createPaginationContainer(), which requires a lot of config, but then you get this.props.relay.hasMore() and this.props.relay.loadMore(10). Example:

export default createPaginationContainer(
Term,
graphql`
fragment Term_viewer on Viewer {
term(slug: $slug, taxonomy: $taxonomy) {
id
name
slug
taxonomy {
rewrite {
slug
}
labels {
singular
plural
}
}
}
posts(term: $slug, taxonomy: $taxonomy, after: $cursor, first: $count)
@connection(key: "Term_posts") {
edges {
node {
...Post_post
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
`,
{
direction: 'forward',
getConnectionFromProps(props) {
return props.viewer && props.viewer.posts;
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
...fragmentVariables,
count,
cursor,
};
},
getFragmentVariables(vars, totalCount) {
return {
...vars,
count: totalCount,
};
},
query: TermQuery,
}
);

Yes, this configuration is confusing and weird. When Relay Modern first dropped, some of the configuration values were missing from the docs! I have a PR merged in Relay for this. As a trade-off, you just call loadMore() and you’re done. All of the connection merging happens automatically.

Apollo

The data prop always has a method called fetchMore(), but you are responsible for merging the results with your previous results, which can be dangerous and weird. Example:

const Archive = ({ variables, fetchMore = null, posts: { pageInfo, edges } }) =>
<section>
<ul>
{edges.map(({ cursor, node }) =>
<li key={cursor}>
<Post post={node} />
</li>
)}
</ul>
{fetchMore &&
pageInfo.hasNextPage &&
<button
className={styles.button}
onClick={() =>
fetchMore({
variables: {
...variables,
cursor: pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const { edges: previousEdges } = previousResult.viewer.posts;
const { edges: newEdges } = fetchMoreResult.viewer.posts;
const newViewer = {
viewer: {
...fetchMoreResult.viewer,
posts: {
...fetchMoreResult.viewer.posts,
edges: [...previousEdges, ...newEdges],
},
},
};
return newViewer;
},
})}
>
MORE
</button>}
</section>;

Takeaways: Sometimes things are very similar in both frameworks. Other times, they are equally as strange. I would note that both are probably better than the current Bring Your Own Implementation solutions that have been written in jQuery from days of yore.

Conclusion

Rather than spending a ton of time trying to pick the perfect solution before building anything, I have tried both, and am able to create what I want in both. I still want to try Apollo on React Native, and I still want to mix my React Native code with native mobile platform code. I think both will work just fine.

Relay Modern works like a dream in React Native. Using it on the web is possible with the tools I outlined above.

Apollo has an ecosystem around it, and its own ideas about how to do things. Facebook created GraphQL and Relay, but does not actively provide ALL of the tools you need.

My prediction: I could rewrite this post every 6 months with lots of new learnings based on changes and pivots from every corner of the ecosystem. GraphQL and React will probably remain “stable.” I think the frameworks around them are just getting started.

--

--

Scott Taylor

Musician. Staff Engineer at Shopify. Formerly: Lead Software Engineer at the New York Times. Married to Allie.