Gentle introduction to Relay framework (React + GraphQL)

rinatrezyapov

Rinat Rezyapov

Posted on January 20, 2023

Gentle introduction to Relay framework (React + GraphQL)

Table Of Content

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:

  1. 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. oneendpoint
  2. 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. graphqlschema
  3. 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 an authors 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)
Enter fullscreen mode Exit fullscreen mode

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!.

  • Install Node.js

  • 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
Enter fullscreen mode Exit fullscreen mode
  • Then create index.js file and add the code for a simple http server that answers with status code 200 and Hello 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}/`);
});
Enter fullscreen mode Exit fullscreen mode
  • Launch created server by typing in the terminal:
node index
Enter fullscreen mode Exit fullscreen mode

If everything is ok you should get this message:

Server running at http://127.0.0.1:5000/
Enter fullscreen mode Exit fullscreen mode
  • Then open http://127.0.0.1:5000/ in the browser. Server should respond with Hello World.

Adding GraphQL support into our server

  • Install graphql-http npm package
npm i graphql-http
Enter fullscreen mode Exit fullscreen mode
  • 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');
Enter fullscreen mode Exit fullscreen mode
  • Then add the code for the GraphQL schema that has a query called GreetingsQuery with a greetings field that answers Hello World!.
// ./server/index.js

const schema = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: 'GreetingsQuery',
        fields: {
            greetings: {
                type: GraphQLString,
                resolve: () => 'Hello World!',
            },
        },
    }),
});
Enter fullscreen mode Exit fullscreen mode
  • After the schema is created we need to pass it to a handler.
// ./server/index.js

const handler = createHandler({ schema });
Enter fullscreen mode Exit fullscreen mode
  • Then we can use this handler object in http.createServer function to handle all /graphql requests. Replace the previous content of http.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.');
    }
});
Enter fullscreen mode Exit fullscreen mode

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}/`);
});
Enter fullscreen mode Exit fullscreen mode
  • 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"}]}
Enter fullscreen mode Exit fullscreen mode

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 name client.
cd React-Relay-App
npx create-react-app client
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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 in client folder
// ./client/relay.config.js

module.exports = {
    src: "./src",
    language: "javascript",
    schema: "../data/schema.graphql",
    exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"],
  }
Enter fullscreen mode Exit fullscreen mode

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 the client folder and add the following content.
// ./client/.babelrc

{
  "plugins": [
    "relay"
  ]
}
Enter fullscreen mode Exit fullscreen mode
  • Finally, add the following script into scripts section of package.json file.
// ./client/package.json

  "scripts": {
    "relay": "relay-compiler"
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We will get the following error:

 - The `schema` configured for project `default` does not exist at `../data/schema.graphql`.
Enter fullscreen mode Exit fullscreen mode

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 in React-Relay-App folder. Out project structure should look like this:
-- React-Relay-App
---- client
---- server
---- data
Enter fullscreen mode Exit fullscreen mode
  • Then go into server folder and create updateSchema.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);
Enter fullscreen mode Exit fullscreen mode

This will generate schema.graphql file in the ../data/schema.graphql folder.

  • Now we need to export schema from our index.js file.
// ./server/index.js

module.exports = {
    schema
}
Enter fullscreen mode Exit fullscreen mode
  • And let's add update script to package.json file of our server project:
// ./server/package.json


"scripts": {
    "update": "node ./updateSchema"
}
Enter fullscreen mode Exit fullscreen mode
  • After that, run updateSchema script.
npm run update
Enter fullscreen mode Exit fullscreen mode

If everything is ok, then you will get the message:

Finished updating schema \React-Relay-App\data\schema.graphql
Enter fullscreen mode Exit fullscreen mode

And you can find generated schema.graphql file inside data folder with the following content:

schema {
  query: GreetingsQuery
}

type GreetingsQuery {
  greetings: String
}
Enter fullscreen mode Exit fullscreen mode

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 the data folder, let's go to client folder and run npm run relay:
cd client
npm run relay
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode

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 in client/src. Indside relay folder create environment.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>
    );
}

Enter fullscreen mode Exit fullscreen mode

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 with RelayEnvironmentProvider:
// ./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>
);
Enter fullscreen mode Exit fullscreen mode

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 create AppQuery outside App component.
// ./client/src/App.jsx

import graphql from 'babel-plugin-relay/macro';

const AppQuery = graphql`
  query AppQuery {
    greetings,
  }
`;

function App() {
...
Enter fullscreen mode Exit fullscreen mode

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,
    {},
  );
...
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode

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! 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:

  1. Repo with the code
  2. Official GraphQL Docs
  3. Official Relay Docs
  4. Building The New Facebook With React and Relay talk
💖 💪 🙅 🚩
rinatrezyapov
Rinat Rezyapov

Posted on January 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related