React, Relay and GraphQL: Under the Hood of the Times Website Redesign
The New York Times website is changing, and the technology we use to run it is changing too.
As the new site rolls out over the next several months, a look under the hood will reveal a number of modern technologies designed to make the site faster and easier to use — for readers, most importantly, but also for our developers.
At the center of this has been our adoption of React, Relay and GraphQL.
The problem we’re solving
More than a year ago, when we first started talking about the technology that would power our new website, simplifying our stack was one of our biggest priorities.
Our current desktop and mobile websites are written in entirely different languages: The desktop is predominately PHP; mobile runs on Node. Other products, such as our foreign-language sites, run on their own unique codebases (Español, 中文 (Chinese). Some do not even rely on our standard CMS, Scoop. All these sites read data from different origins and endpoints in different ways. It is hard to find a common denominator between them all.
If I want to make a new app tomorrow, chances are I need to:
- Obtain credentials for multiple web services
- Write an HTTP client (for the umpteenth time) to talk to said services
- Create my view layer, probably from scratch, because there is no real central repository for NYT components
We thought it would be nice if there was one place to add and retrieve data and one way to authenticate against it. It would also be helpful if there was a common language and repository for creating and reusing components. If a new developer joins our team, I want to point them at a single page of documentation that explains how to get up and running — and preferably start building apps the same day.
This is not at all a dream scenario. We are moving towards this reality. That future is Relay and GraphQL.
GraphQL and Relay
Relay is an open source project by Facebook that exposes a framework they have been using internally for years. It is the glue that binds components written in React to data fetched from a GraphQL server. Relay is written in JavaScript, and we are using it as the basis for our new website’s codebase to power our desktop and mobile versions as one on Node.
GraphQL is “a query language for APIs”, which has a default implementation in Node. Facebook developed it to provide a data source that can evolve without breaking existing code and to favor speed on low-powered and low-quality mobile devices. The schema can evolve, but should never break. Products are described in graphs and queries, instead of the REST notion of endpoints.
It works like this: GraphQL queries contain nodes, and only the nodes that are requested are returned in a given response. GraphQL nodes do not have to represent a flat data structure — each node can be resolved in a custom manner. Here is a simple example of a GraphQL query:
{
me {
name
age
friends {
id
name
}
}
}
It doesn’t matter how the query is resolved. The hard initial work is designing it in a way that will survive redesigns, backend migrations and framework changes.
A query might be resolved by multiple data sources: REST APIs, database, a flat JSON file. A product might begin by returning data from a simple CSV file, and later be grow to return data from a cluster of databases or remote storage like BigTable.
GraphQL is simply a clearinghouse for queries. It also comes with a tool called GraphiQL that allows you to view and debug your queries visually. And Facebook has open-sourced a library, called DataLoader, that makes it easy to query multiple backends asynchronously without having to write custom Promise logic that ends up in callback hell.
Relay acts as a partner to GraphQL and React. A top-level query usually happens on a route — a URL pattern that loads a component when it is matched.
// queries/Page.js
import { graphql } from 'react-relay';
const PageQuery = graphql`
query Page_Query($slug: String!) {
viewer {
...Page_viewer
}
}
`;
// routes/index.js
import Route from 'found/lib/Route';
import Page from 'routes/Page';
import PageQuery from 'queries/Page';
<Route
path=":slug"
Component={Page}
query={PageQuery}
render={renderProp}
/>
GraphQL “fragments” are co-located with your React components. A component describes what slices of data it needs on certain types. Relay queries “spread” the fragments of other components. In this particular case, the “slug” is extracted from the URL path and passed to our GraphQL query. The Page component will be populated with a “viewer” prop that contains the data specified below:
// routes/Page/index.js
import { graphql, createFragmentContainer } from 'react-relay';
import styles from './Page.scss';
const Page = ({ viewer: { page } }) => {
if (!page) {
return <Error />;
}
const { title, content, featuredMedia } = page;
return (
<article className={styles.content}>
<header>
<h1 className={styles.title}>{title}</h1>
</header>
{featuredMedia && <Media media={featuredMedia} />}
<section dangerouslySetInnerHTML={{ __html: content }} />
</article>
);
};
export default createFragmentContainer(
Page,
graphql`
fragment Page_viewer on Viewer {
page(slug: $slug) {
title
content
featuredMedia {
... on Image {
source_url
}
...Media_media
}
}
}
`
);
As React components become more nested, queries can become increasingly complex. In Relay Classic, all of the query-parsing logic happened at runtime. As of Relay Modern, queries are now parsed at build time and are static at runtime. This is great for performance.
One Caveat
Migrating from Classic to Modern can be a big lift. The project has provided a compatibility guide to allow your code to incrementally adopt new features, but the fragmented nature of the Node ecosystem can make this complex. Your codebase might be on the latest version, but some of your dependencies might be pinned to an earlier version.
We handle a lot of the complexity around upgrades and dependencies using our open source project, kyt. Relay Modern is such a massive improvement that it requires a “cutover” of old to new code.
However, the benefits are exciting. By default, GraphQL queries are sent to the server by Relay as an HTTP POST body containing the text of a query and the variables needed to fulfill it. Queries compiled by Relay Modern at build time can be persisted to a datastore, and IDs can be sent to the GraphQL server instead. We look forward to taking advantage of this optimization.
It has been exciting moving our codebase to React, and leaning on the great features kyt provides out of the box, such as CSS Modules. We are finally creating the central component repository we’ve longed for.
As we transition away from using REST APIs, we no longer have to query the canonical representation of an article, when all we really need in some display modes is five to seven fields.
When we want to update our design across all products, we will no longer have to make changes across several codebases. This is the reality we are moving towards. We think Relay and GraphQL are the perfect tools to take us there.
Scott Taylor is a senior software engineer on the Web Frameworks team.