Using Create React App with Relay Modern

Written by tarkus | Published 2017/08/09
Tech Story Tags: react | relay | web-development

TLDRvia the TL;DR App

This tutorial assumes that you’re already familiar with Create React App (CRA) and a little bit of Relay, and just looking for some examples of how to use them together. Here you will learn:

  • How to modify Create React App setup to use “babel-plugin-relay” in its config
  • How to configure Relay Compiler
  • How to use Relay’s <QueryRenderer /> component
  • How to fetch data with Relay as the user navigates though the site

Note, that many tutorials on the Internet covering the previous version of Relay (aka Relay Classic). It differs a lot from Relay Modern, so it’s better to just ignore those not to confuse yourself.

TL;DR — Visit [**kriasoft**](https://github.com/kriasoft)/[**react-static-boilerplate**](https://github.com/kriasoft/react-static-boilerplate) repository on GitHub for a complete Create React App + Relay Modern example.

Injecting “babel-plugin-relay” into CRA

As you probably know, Create React App is not very extensible. Basically, you are left with these main options when it comes to customizing Babel/Webpack configs:

Options 1: Run yarn eject and tweak Babel/Webpack/PostCSS etc. configs in-place right in your project. But in that case, you will not be able to easily upgrade your app to a new version of React-related tooling. This defeats the purpose of using Create React App in the first place, as opposed to using some popular boilerplate, such as React Starter Kit (RSK), which you can clone and yet being able to pull and merge updates back into your project down the road.

Option 2: Fork create-react-app repository, and use a patched version of the “react-scripts”. Note that keeping your fork up to date could consume some of your free time which you could better spend on something more useful. See React App SDK as an example, that allows extending CRA with custom config overrides and comes with a server-side code support.

Option 3: Create a script that will automatically patch your “react-scripts” npm module whenever you run yarn build or yarn start. I personally find this option more practical as it helps me get things done faster during the initial setup as well as keeping my project up to date with lesser amount of efforts in the future.

After bootstrapping a new project (by running yarn create react-app), you will need to install Relay’s run-time and dev dependencies:

$ yarn add relay-runtime react-relay$ yarn add relay-compiler babel-plugin-relay --dev

Next, create “setup.js” file in the root of your project with the following contents:

const fs = require('fs');const path = require('path');const file = path.resolve('./node_modules/babel-preset-react-app/index.js');const text = fs.readFileSync(file, 'utf8');if (!text.includes('babel-plugin-relay')) {  if (text.includes('const plugins = [')) {    text = text.replace(      'const plugins = [',      "const plugins = [\n  require.resolve('babel-plugin-relay'),",    );    fs.writeFileSync(file, text, 'utf8');  } else {    throw new Error(`Failed to inject babel-plugin-relay.`);  }}

Finally, update the “scripts” section in package.json file to run this script before react-scripts <command>:

"scripts": {  "build": "node ./setup && react-scripts build",  "test": "node ./setup && react-scripts test --env=jsdom",  "start": "node ./setup && react-scripts start"}

Now, whenever you run yarn build, yarn test, or yarn start the Babel compiler will use “babel-plugin-relay” as required by Relay Modern.

Configuring Relay Compiler

The next step is to configure Relay Compiler, the purpose of which is to find all the graphql`…` queries in your code, pre-compile them and save to __generated__/*.graphq.js files consumed by “babel-plugin-relay” from the previous step.

In order to do so, first, you need to save the text-based representation of your GraphQL schema into a file inside of your project, assuming that your React app is intended to work with some external GraphQL API. And then add one more npm script into package.json that will run relay-compiler. It may look something like this:

"scripts": {  ...  "relay": "curl https://graphql-demo.kriasoft.com/schema -o ./src/graphql.schema && relay-compiler --src ./src --schema ./src/graphql.schema}

For this exercise, we are going to use an existing GraphQL demo API hosted at https://graphql-demo.kriasoft.com (source code).

Before you can run yarn relay, make sure that you have Facebook’s Watchman tool installed on you dev machine.

Now, whenever you change some GraphQL/Relay query in your code, you would need to run yarn relay over again, or keep it running in a separate shell window in watch mode by running yarn relay -- --watch.

Q: Do I need to excluded generated by Relay Compiler files from my source control by appending __generate__/** to .gitignore?A: Good question :) Yes, as a rule of thumb you don’t want any auto-generated files in your source code repository.

Try putting the following piece of code into any of your source files (e.g. into src/index.js) and running yarn relay && yarn build:

import { graphql } from 'relay-runtime';

graphql`query { me { displayName } }`;

If you can build the project without any errors, most likely that the previous two steps are done correctly.

How to initialize a Relay client (environment)?

This part is simple, just follow the official docs. Your Relay client (environment) will look something like this (src/relay.js):

You can even use it directly without “react-relay” package if you want to. Here is an example:

import { graphql, fetchQuery } from 'relay-runtime';import relay from './relay';

fetchQuery(relay,graphql`query { me { displayName } }`).then(...);

In a real-world project, you would need to replace the hardcoded API URL in the code sample above with something like this:

fetch(process.env.REACT_APP_API || 'http://localhost:8080', ...)

..so you could easily run your app in dev/test/prod environments. For more information about injecting environment variables into your app, refer to Create React App user guide.

How to use <QueryRenderer /> component

Relay Modern comes with a handy <QueryRenderer /> component that you can use at the top level of your app, somewhat similarly to Redux’s <Provider> component. It will pass Relay’s environment (including Relay store and HTTP client) down to all the children React components via context. Also it will handle updates for you, so you won’t need to re-render your components manually after running mutations.

The logic looks like this — whenever a user opens your site, or navigates from one page / screen to another, you find the corresponding GraphQL query and a list of parameters (variables) for that page, pass it to <QueryRenderer> and let it do the job (it will fetch missing data and re-render affected children component).

Your top-level React component (src/App) will look something like this:

The code sample above uses “history” npm module for navigation and “universal-router” for resolving URL path to a route. The contents of src/history.js looks as follows:

import createHistory from 'history/createBrowserHistory';export default createHistory();

It is a cross-browser wrapper around HTML5 History API, that allows you to respond to any changes caused by changes in window.location initiated on the client-side, as well as modifying that state, e.g. by calling history.push(..).

The contents of src/router.js is a little bit more interesting. Given the list of application routes and a global router handler function, it initializes and exports an instance of the universal-router class.

BTW, in case with GraphQL/Relay it makes sense to use declarative routing approach (as opposed to imperative routes), meaning that all your routes contain just metadata and no handlers. At a bare minimum, each of your route can contain a URL path string (pattern), GraphQL query, and component to render, for example:

const routes = [{path: '/posts/:id',query: graphql`query routeStoryQuery($id: ID!) {post: node(id: $id) { ...Post_post }}`,component: () =>import(/* webpackChunkName: 'post' */, './Post')},...]

Now you can initialize the router by providing the list of routes and a global router handler (resolver) function, somewhat similar to this:

import Router from 'universal-router';import { graphql } from 'relay-runtime';

const routes = [ ... ]; // see above

export default new Router(routes, {resolveRoute({ route, next }, params) {if (!route.component) next();return {query: route.query,component: route.component(),variables: params,};}});

You would need to tweak this example a little to cover more use cases. The idea is that when the corresponding route is found (URL matched), it should start downloading the JavaScript chunk for that page / screen and at the same time pass query and variables to QueryRenderer (see src/App example above), effectively triggering data fetching for that route. The only issue, is that when QueryRenderer has finished fetching data, you need to make sure that async chunk loading has also finished its work (promise resolve) before you can render the page, for that purpose you use an intermediate (AppRenderer) component.

You can put as much or as little metadata in your routes to suite your app, for example, you can put authentication rules like so:

{path: '/admin/users',query: graphql`...`,component: () => ...,authorize: { roles: ['admin'] } // or, "authorize: true" etc.}

With GraphQL / Relay you may want to fetch data for all of your components on a particular page / screen at once (as opposed to fetching data individually for each nested level of components), otherwise you would degrade the value of GraphQL a little.

For a complete example of using Create React App with Relay Modern please visit [**kriasoft**](https://github.com/kriasoft)/[**react-static-boilerplate**](https://github.com/kriasoft/react-static-boilerplate) — it’s a popular boilerplate for creating single-page apps on React/Relay stack, things like admin panels, dashboards etc. where you don’t need server-side rendering. You can use it not only as an example, but a seed for your React projects to save a good amount of time configuring everything from scratch. Feel free to ping me on Gitter or Twitter if you bump into any issues with it, I’d be glad to help.

P.S.: There is an OpenCollective page for this project :) Your support is more than welcome!


Published by HackerNoon on 2017/08/09