Gentle introduction to Relay framework (React + GraphQL)
Rinat Rezyapov
Posted on January 20, 2023
Table Of Content
- Introduction
- What is Relay?
- What is GraphQL?
- Why GraphQL is better than Rest API?
- If GraphQL is better why Rest API is still here?
- Some frequent questions from beginners or prejudices
- Let's build React app with Relay
- Structure of the project
- Creating Node.js http server
- Adding GraphQL support into our server
- Creating React + Relay application
- Generating schema.graphql file
- Creating GraphQL query in our client app
- Update Component
Introduction
This post aims to gently introduce React developers to Relay framework. We'll start with a brief description of Relay and GraphQL (in simple terms) and then we'll create a simple React app that makes one simple query. This is a very beginner-friendly introduction so I intentionally avoid deep dives or advanced examples. Let's start!
What is Relay?
In short, Relay is a framework that helps you to incorporate GraphQL query language into React applications.
What is GraphQL?
In simple terms, GraphQL is a better version of Rest API (this is the author's opinion and not an assertion).
Why GraphQL is better than Rest API?
For a number of reasons:
- GraphQL has one endpoint instead of multiple. This helps frontend developers to develop application more quickly because there is no need to write new requests to backend or change old ones.
- GraphQL uses a schema that describes entities and their types. This schema is synced between the frontend and the backend. GraphQL also generates documentation that allows frontend developers to compose queries without the help of backend developers or other manually created documentation.
- You can request only the needed fields of an entity and request a set of different entities as one request. Imagine you fetched a
book
entity which has anauthors
field with an array of author ids. Now you need to make another request to fetch information about authors by id. This will create a waterfall problem in your React application. Relay and GraphQL handle this for you.
If GraphQL is better why Rest API is still here?
Moving from Rest API to GraphQL is a quite complex process. First of all, frontend and backend developers need to learn it and be on the same page. Secondly, public APIs can't just move to GraphQL overnight because the API consumers still use Rest API clients. Although, some public APIs can afford to have Rest API and GraphQL at the same time. And if your API is not public facing, after some preparations, you can gradually adopt GraphQL like it was done by Facebook. Finally, it's just hard to replace such a fundamental approach as Rest API. Perhaps, GraphQL will just occupy its niche as it was in the case of WebSockets.
Some frequent questions from beginners or prejudices
Q. Do you still need a state management library if you use Relay?
A. No, Relay can completely replace state management libraries such as Redux.
Q. Is Relay and GraphQL slower than Rest API?
A. Not as you can notice in the web application. There is some additional computational overhead regarding schema and typing system which could affect real-time applications or real-time game applications but for such applications, you wouldn't use JavaScript, let alone React, Relay, GraphQL...
Q. Does Relay (GraphQL) has Database requirements?
A. No, Relay (GraphQL) is agnostic to the data source.
Let's build React app with Relay
Structure of the project
-- React-Relay-App
---- client (Create React App)
---- server (Node.js + http)
Creating Node.js http server
Let's start with the server. We will create a simple Node.js server that will have GreetingsQuery
which will answer with Hello World!
.
Go to
React-Relay-App -> server
folder and initialize npm (you need it to install npm packages later).
cd React-Relay-App
cd server
npm init
- Then create
index.js
file and add the code for a simple http server that answers with status code200
andHello World
text for now.
// ./server/index.js
const http = require('http');
const hostname = '127.0.0.1';
const port = 5000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
- Launch created server by typing in the terminal:
node index
If everything is ok you should get this message:
Server running at http://127.0.0.1:5000/
- Then open
http://127.0.0.1:5000/
in the browser. Server should respond withHello World
.
Adding GraphQL support into our server
- Install
graphql-http
npm package
npm i graphql-http
- Import all necessary assets to create GraphQL Schema into
index.js
file.
// ./server/index.js
const { GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql');
const { createHandler } = require('graphql-http/lib/use/node');
- Then add the code for the GraphQL schema that has a query called
GreetingsQuery
with agreetings
field that answersHello World!
.
// ./server/index.js
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'GreetingsQuery',
fields: {
greetings: {
type: GraphQLString,
resolve: () => 'Hello World!',
},
},
}),
});
- After the schema is created we need to pass it to a handler.
// ./server/index.js
const handler = createHandler({ schema });
- Then we can use this handler object in
http.createServer
function to handle all/graphql
requests. Replace the previous content ofhttp.createServer
function with the following.
// ./server/index.js
const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql')) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Request-Method', '*');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
res.setHeader('Access-Control-Allow-Headers', '*');
if ( req.method === 'OPTIONS' ) {
res.writeHead(200);
res.end();
return;
}
handler(req, res);
} else {
res.writeHead(200).end('Please, use /graphql suffix.');
}
});
Ignore res.setHeader
invocations since their purpose is to configure CORS and is not related to GraphQL directly.
- Final result of our server code
// ./server/index.js
const http = require('http');
const { GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql');
const { createHandler } = require('graphql-http/lib/use/node');
const hostname = '127.0.0.1';
const port = 5000;
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'GreetingsQuery',
fields: {
greetings: {
type: GraphQLString,
resolve: () => 'Hello World!',
},
},
}),
});
const handler = createHandler({ schema });
const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql')) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Request-Method', '*');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
res.setHeader('Access-Control-Allow-Headers', '*');
if ( req.method === 'OPTIONS' ) {
res.writeHead(200);
res.end();
return;
}
handler(req, res);
} else {
res.writeHead(200).end('Please, use /graphql suffix.');
}
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
- Now let's re-start our server and open
http://127.0.0.1:5000/graphql
in the browser. You should get the following message:
{"errors":[{"message":"Missing query"}]}
Which is completely ok. GraphQL expects valid query, you can't make a request to a endpoint and get response, like you would do with GET method in Rest API.
Creating React + Relay application
Now, since our server is ready to answer GraphQL queries, we can start with our client-side part.
I will use Create React App tool to generate React application, but the steps below can be applied to any other React web application or even React Native mobile application.
- Go to
React-Relay-App
folder and create React project with the nameclient
.
cd React-Relay-App
npx create-react-app client
- After the generation process is finished, go to the
client
folder and install all necessary packages.
cd client
npm i graphql react-relay relay-compiler babel-plugin-relay
graphql - the main GraphQL npm package
react-relay - the main Relay npm package
relay-compiler - goes through all your client files and parses GraphQL code
babel-plugin-relay - helps JavaScript to understand GraphQL code
- Next, create
relay.config.js
file inclient
folder
// ./client/relay.config.js
module.exports = {
src: "./src",
language: "javascript",
schema: "../data/schema.graphql",
exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"],
}
src
points to the folder where React components with GraphQL code will be located.
schema
points to the shared between backend and frontend schema file. We will return to it later.
- Also, create
.babelrc
file in theclient
folder and add the following content.
// ./client/.babelrc
{
"plugins": [
"relay"
]
}
- Finally, add the following script into
scripts
section ofpackage.json
file.
// ./client/package.json
"scripts": {
"relay": "relay-compiler"
}
We will launch this script every time we add new GraphQL code to our React components. For simplicity, we will do it manually.
- Now let's launch Relay script that we added before.
npm run relay
We will get the following error:
- The `schema` configured for project `default` does not exist at `../data/schema.graphql`.
That means that Relay on the client side expects schema.graphql
from backend side to correctly validate GraphQL code in React components. In my opinion, this is the most important part of GraphQL because it completely elliminates misunderstandings between frontend and backend developers when using API and creates a strong contract between them.
Now let's go back to server and create a script that generates schema.graphql
file.
Generating schema.graphql file
- First, let's create
data
folder inReact-Relay-App
folder. Out project structure should look like this:
-- React-Relay-App
---- client
---- server
---- data
- Then go into
server
folder and createupdateSchema.js
file with the following content.
// ./server/updateSchema.js
const fs = require('fs');
const path = require('path');
const { schema } = require('./index');
const { printSchema } = require('graphql');
const schemaPath = path.resolve(__dirname, '../data/schema.graphql');
fs.writeFileSync(schemaPath, printSchema(schema));
console.log('Finished updating schema ' + schemaPath);
This will generate schema.graphql
file in the ../data/schema.graphql
folder.
- Now we need to export
schema
from ourindex.js
file.
// ./server/index.js
module.exports = {
schema
}
- And let's add
update
script topackage.json
file of our server project:
// ./server/package.json
"scripts": {
"update": "node ./updateSchema"
}
- After that, run
updateSchema
script.
npm run update
If everything is ok, then you will get the message:
Finished updating schema \React-Relay-App\data\schema.graphql
And you can find generated schema.graphql
file inside data
folder with the following content:
schema {
query: GreetingsQuery
}
type GreetingsQuery {
greetings: String
}
Now, it's time to go back to our client and launch npm run relay
one more time.
Creating GraphQL query in our client app
- Now that we have
schema.graphql
file in thedata
folder, let's go toclient
folder and runnpm run relay
:
cd client
npm run relay
- You should get the following message:
> client@0.1.0 relay
> relay-compiler
[INFO] [default] compiling...
[INFO] [default] compiled documents: 0 reader, 0 normalization, 0 operation text
[INFO] Done.
That means relay-compiler
went through all files in client/src
folder and didn't find GraphQL code, so there is nothing to generate. Let's fix it!
- Create
relay
folder inclient/src
. Indsiderelay
folder createenvironment.js
file with the following content.
// ./client/src/relay/environment.js
import React from 'react';
import {
Store,
RecordSource,
Environment,
Network,
Observable,
} from "relay-runtime";
import { RelayEnvironmentProvider } from "react-relay";
const fetchFn = (params, variables) => {
const response = fetch("http://localhost:5000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: params.text,
variables,
}),
});
return Observable.from(response.then((data) => data.json()));
};
function createEnvironment() {
const network = Network.create(fetchFn);
const store = new Store(new RecordSource());
return new Environment({ store, network });
}
export default function RelayEnvironment({ children }) {
const environment = React.useMemo(() => {
return createEnvironment();
}, []);
return (
<RelayEnvironmentProvider environment={environment}>
{children}
</RelayEnvironmentProvider>
);
}
Don't focus too much on these settings as they are made only once at the beginning of a project. What it does is creates wrapper around fetch
and then passes it to Relay library which handles network requests. Then we create RelayEnvironment
provider to use it as wrapper around our root App
component.
- Now, let's go to
client/src/index.js
file and wrapp out React application withRelayEnvironmentProvider
:
// ./client/src/relay/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import RelayEnvironment from './relay/environment';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RelayEnvironment>
<React.Suspense fallback="Loading...">
<App />
</React.Suspense>
</RelayEnvironment>
</React.StrictMode>
);
Notice, that we use React Suspense
here which is a must when you use Relay.
- We're almost done. Let's go to
App
component and create GraphQL request. We createAppQuery
outsideApp
component.
// ./client/src/App.jsx
import graphql from 'babel-plugin-relay/macro';
const AppQuery = graphql`
query AppQuery {
greetings,
}
`;
function App() {
...
Relay enforces several rules, one is that we must start the name of a query with a component's name, so result should be AppQuery
.
And then inside App
component we use hook useLazyLoadQuery
to make a query.
// ./client/src/App.jsx
function App() {
const data = useLazyLoadQuery(
AppQuery,
{},
);
...
After that we render data
result in our App
component as {data.greetings}
. The full App
component should look like this:
// ./client/src/App.jsx
import { useLazyLoadQuery } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
import logo from './logo.svg';
import './App.css';
const AppQuery = graphql`
query AppQuery {
greetings,
}
`;
function App() {
const data = useLazyLoadQuery(
AppQuery,
{},
);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
<code>{data.greetings}</code>
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
and
<a
className="App-link"
href="https://relay.dev/docs/"
target="_blank"
rel="noopener noreferrer"
>
Relay
</a>
</header>
</div>
);
}
export default App;
- Ok, we added GraphQL query. Now we can start the app and see results! Not so fast! After adding GraphQL code into React components we should run
npm run relay
so it can autogenerate needed code for runtime.
npm run relay
> client@0.1.0 relay
> relay-compiler
[INFO] [default] compiling...
[INFO] [default] compiled documents: 1 reader, 1 normalization, 1 operation text
[INFO] Done.
Notice that inside src
folder the folder name __generated__
appeared. That's the runtime code that Relay is needed to function correctly. You don't need to look inside.
- Now is the time to start server and client and see results!
Congratulations! We did it! I hope now you see why I choose to make this example as easy as possible. Even given that, the post turned out to be quite big.
Anyway, that's just 10% of the capabilities of Relay framework and GraphQL and ahead of us are mutations, arguments, variables, fragments and many more.
Links:
Posted on January 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.