Industry NewsT2 Dark_1200x303

When creating a React app that will use a GraphQL API, you need a client that can help scale while maintaining performance. Let’s explore Relay.

The GraphQL ecosystem has been growing wildly in the last years. There’s always new content being published, new tutorials being written, new libraries being created. Overall we can say that the usage of GraphQL is becoming popular.

GraphQL can be powerful, especially for React apps. When creating a new React app that’s going to use a GraphQL API, having a nice GraphQL client that can help scale and keep the app performant at the same time makes a big difference.

Created by Facebook, Relay is a very powerful and robust JavaScript framework for declaratively working with GraphQL and building data-driven React applications. It was built to be very performant and highly scalable, with scalability top of mind. It has a lot of features, such as data consistency, optimistic updates, type safety, optimized runtime, etc.

Relay

We are going to use the newest version of Relay. It has support for React Hooks and it’s very powerful and easy to use.

We are going to new create-react-app project:

npx create-react-app graphql-relay-example

In order to start using Relay, we need to install a few dependencies.

yarn add @chakra-ui/icons @chakra-ui/layout @chakra-ui/react @emotion/react babel-plugin-relay framer-motion isomorphic-fetch relay-hooks relay-runtime

Now we need to install some development dependencies.

yarn add --dev @types/isomorphic-fetch @types/react-relay @types/relay-runtime get-graphql-schema graphql react-relay relay-compiler relay-compiler-language-typescript

We now have installed all the dependencies that we are going to need. Now, inside our package.json file, let’s add two new scripts:

"get-graphql-schema": "get-graphql-schema https://podhouse-server.herokuapp.com/graphql > schema.graphql --graphql",
"relay": "relay-compiler --src ./src --schema ./schema.graphql --language typescript"

The get-graphql-schema script is for generating our schema.graphql file from our GraphQL API schema. The Relay script is for converting our GraphQL literals into generated files.

Now, inside our src folder, we are going to create a new file called relay. Inside this file, we are going to create our Relay environment.

import {
  Environment,
  Network,
  RecordSource,
  Store,
  RequestParameters,
  Variables
} from "relay-runtime";
import fetch from "isomorphic-fetch";

function fetchQuery(operation: RequestParameters, variables: Variables) {
  return fetch('https://podhouse-server.herokuapp.com/graphql', {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-type": "application/json",
    },
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
  }).then((response: any) => {
    return response.json()
  })
};

const network = Network.create(fetchQuery);

const env = new Environment({
  network,
  store: new Store(new RecordSource(), {
    gcReleaseBufferSize: 10,
  }),
});

export default env;

The API that we are using here is a podcast API. After creating our Relay environment, we are going to pass it inside our index file. We need to import the RelayEnvironmentProvider and pass our Environment as an environment.

The RelayEnvironmentProvider is used for setting Relay to a React Context. The recommendation here is to always use a single instance of this components, at the root of your app.

import React from 'react';
import ReactDOM from 'react-dom';
import { RelayEnvironmentProvider } from "react-relay";
import { ChakraProvider } from "@chakra-ui/react";
import App from './App';
import reportWebVitals from './reportWebVitals';
import Environment from "./relay";
ReactDOM.render(
<RelayEnvironmentProvider environment={Environment}>
<ChakraProvider>
<React.StrictMode>
<App />
</React.StrictMode>
</ChakraProvider>
</RelayEnvironmentProvider>,
document.getElementById('root')
);

We have successfully set up Relay inside our application. Now we are going to learn more about using it in practice.

Query

The useLazyLoadQuery is the simplest hook for querying something with Relay. All you need to do is pass a GraphQL query using the graphql template and variables (in case it’s needed).

import React from 'react';
import { useLazyLoadQuery } from "react-relay";
;
import { PodcastQuery } from "./__generated__/PodcastQuery.graphql";

const query = graphql`
  query PodcastQuery($_id: ID!) {
    podcast(_id: $_id) {
      id
      _id
      name
      author
      description
      website
      rss
      image
    }
  }
`;

const App = () => {
  const data = useLazyLoadQuery<PodcastQuery>(query, { id: 4 }, { fetchPolicy: 'store-or-network' });
  return <h1>{data.podcast?.name}</h1>;
}

The useLazyLoadQuery hook has a few differences from the QueryRenderer, but one of the most important is the necessity to use React Suspense for rendering loading states while the data is loading. Another big difference is that you don’t to pass your Relay environment to useLazyLoadQuery like QueryRenderer did.

According to the Relay documentation, the useLazyLoadQuery hook can trigger some multiple unnecessary round trips. That’s why it’s recommended to use the usePreloadedQuery hook instead, which is the hook that we are going to use. Let’s understand now how the usePreloadedQuery hook works in practice.

Let’s create a new folder called components and create a new folder called PodcastItem. Inside this new folder, we are going to create a new file called PodcastItem. We will use this file for rendering each podcast that we fetch from our API.

This is how our PodcastItem is going to look:

import React from "react";
import { Link as ReactRouterLink } from "react-router-dom";
import { Grid, GridItem, Image, Heading, Text } from "@chakra-ui/react";

interface Props {
  node: {
    readonly id: string;
    readonly _id: string;
    readonly name: string;
    readonly description: string;
    readonly image: string;
  } | null;
};

const PodcastItem = ({ node }: Props) => (
  <Grid
    h="auto"
    templateRows="repeat(2, max-content)"
    templateColumns="80px 1fr"
    gap={4}
  >
    <GridItem w="80px" h="80px" rowSpan={2} colSpan={1}>
      <ReactRouterLink to={{ pathname: `/podcast/${node?._id}`, state: { _id: node?._id } }}>
        <Image src={node?.image} alt="Podcast image" />
      </ReactRouterLink>
    </GridItem>
    <GridItem colSpan={2}>
      <ReactRouterLink to={{ pathname: `/podcast/${node?._id}`, state: { _id: node?._id } }}>
        <Heading size="md" letterSpacing="-0.03em" cursor="pointer">
          {node?.name}
        </Heading>
      </ReactRouterLink>
    </GridItem>
    <GridItem colSpan={2}>
      <Text>{node?.description}</Text>
    </GridItem>
    <GridItem colSpan={4} />
  </Grid>
);

export default PodcastItem;

Now, inside our src folder, we are going to create two new files called Podcasts and PodcastsResults.

Inside our Podcasts file, we will use the usePreloadedQuery to query a list of podcasts. First, we need to import the useQueryLoader from react-relay.

import { useQueryLoader } from "react-relay";`

Now we will create our query using the graphql template literal. Our query is going to be named PodcastsQuery, and we are going to pass a fragment inside it. I know you’re wondering, “Why pass a fragment,” right? It will make sense later, trust me.

const podcastsQuery = graphql`
  query PodcastsQuery {
    ...PodcastsResults_podcasts
  }
`;

Now inside our component, we are going to use the useQueryLoader hook. It returns three values: the query reference, a loadQuery function that when executed will load our query, and a disposeQuery function which when executed will set our query reference to null.

const [queryReference, loadQuery, disposeQuery] = useQueryLoader<_PodcastsQuery_>(
podcastsQuery
);

We are going to use our loadQuery function every time our component mounts, and use the disposeQuery every time our component unmounts.

useEffect(() => {
loadQuery({});
return () => {
disposeQuery();
};
}, [loadQuery, disposeQuery]);

Remember that we need to use React Suspense? So, to render it, we need to wrap it inside a Suspense component. We are going to show a simple fallback component just to show the content is loading.

Now we are going to import our PodcastsResults components and pass it inside the Suspense component. We will pass our podcastsQuery and queryReference as props.

After all that work, our Podcasts file will look like this:

import React, { useEffect, Suspense } from "react";
import graphql from "babel-plugin-relay/macro";
import { useQueryLoader } from "react-relay";
import { PodcastsQuery } from "./__generated__/PodcastsQuery.graphql";
import PodcastsResults from "./PodcastsResults";

const podcastsQuery = graphql`
  query PodcastsQuery {
    ...PodcastsResults_podcasts
  }
`;

const Podcasts = () => {
  const [queryReference, loadQuery, disposeQuery] = useQueryLoader<PodcastsQuery>(
    podcastsQuery
  );

  useEffect(() => {
    loadQuery({});
    return () => {
      disposeQuery();
    };
  }, [loadQuery, disposeQuery]);

  return (
    <div>
      {queryReference && (
        <Suspense fallback={<h1>Loading...</h1>}>
          <PodcastsResults
            podcastsQuery={podcastsQuery}
            queryReference={queryReference}
          />
        </Suspense>
      )}
    </div>
  )
};

export default Podcasts;

Our Podcasts component is now ready. Now we will work on our PodcastsResults component and understand a little bit more about how the usePreloadedQuery hook works and learn about a new hook for pagination.

Pagination

Inside our PodcastsResults file, we are going to use two different Relay hooks. But first, we need to import a few things:

import React, { useCallback } from "react";
import { VStack, StackDivider, Button } from "@chakra-ui/react"
import graphql from "babel-plugin-relay/macro";
import { GraphQLTaggedNode } from "react-relay";
import {  
usePreloadedQuery,  
usePaginationFragment,  
PreloadedQuery,  
} from "react-relay";
import PodcastItem from "../../components/PodcastItem/PodcastItem";
import { PodcastsQuery } from "./__generated__/PodcastsQuery.graphql";
import { PodcastsPaginationQuery } from "./__generated__/PodcastsPaginationQuery.graphql";
import { PodcastsResults_podcasts$key } from "./__generated__/PodcastsResults_podcasts.graphql";

We want to query a list of podcasts and display them on our app. We don’t want to query all the podcasts, otherwise the API would take too long and the database would receive a lot of requests, which may cause a problem. We want to get a list of podcasts and paginate through them—very simple.

The usePaginationFragment hook is perfect for that. We render a GraphQL fragment which has a @connection and paginate over it.

Remember that we passed a GraphQL fragment inside our query? That’s because we are going to use the usePaginationFragment hook for pagination. Now we need to create our GraphQL fragment using the graphql template, which is going to look like this:

const fragment = graphql`
fragment PodcastsResults_podcasts on Query
@argumentDefinitions(
after: { type: "String" }
first: { type: "Int", defaultValue: 20 }
before: { type: "String" }
last: { type: "Int" }
)
@refetchable(queryName: "PodcastsPaginationQuery") {
podcasts(
after: $after
first: $first
before: $before
last: $last
) @connection(key: "Podcast_podcasts") {
edges {
node {
id
_id
name
description
image
}
}
}
}
`;

Next we are going to use the usePreloadedQuery. We will pass our podcastsQuery and queryReference, both of which we had passed as props to our component. We now have our podcasts, but we need to create a way for paginating through them, so we need to use the usePaginationFragment.

const query = usePreloadedQuery<_PodcastsQuery_>(_podcastsQuery_, _queryReference_);

The usePaginationFragment hook receives both our fragment and our query as response values and returns an object with our data and a few callbacks.

const { data, loadNext, isLoadingNext } = usePaginationFragment<
_PodcastsPaginationQuery_,
_PodcastsResults_podcasts$key_
>(fragment, query);

We are going to create a function called loadMore for loading more podcasts, which is going to look like this:

 const loadMore = useCallback(() => {
  if (isLoadingNext) return;
  loadNext(20);
}, [isLoadingNext, loadNext]); 

After all that, our PodcastsResults component is going to look like this:

import React, { useCallback } from "react";
import { VStack, StackDivider, Button } from "@chakra-ui/react"
import graphql from "babel-plugin-relay/macro";
import { GraphQLTaggedNode } from "react-relay";
import {
  usePreloadedQuery,
  usePaginationFragment,
  PreloadedQuery,
} from "react-relay";
import PodcastItem from "../../components/PodcastItem/PodcastItem";
import { PodcastsQuery } from "./__generated__/PodcastsQuery.graphql";
import { PodcastsPaginationQuery } from "./__generated__/PodcastsPaginationQuery.graphql";
import { PodcastsResults_podcasts$key } from "./__generated__/PodcastsResults_podcasts.graphql";

const fragment = graphql`
  fragment PodcastsResults_podcasts on Query
  @argumentDefinitions(
    after: { type: "String" }
    first: { type: "Int", defaultValue: 20 }
    before: { type: "String" }
    last: { type: "Int" }
  )
  @refetchable(queryName: "PodcastsPaginationQuery") {
    podcasts(
      after: $after
      first: $first
      before: $before
      last: $last
    ) @connection(key: "Podcast_podcasts") {
      edges {
        node {
          id
          _id
          name
          description
          image
        }
      }
    }
  }
`;

interface Props {
  podcastsQuery: GraphQLTaggedNode;
  queryReference: PreloadedQuery<PodcastsQuery>;
}

const PodcastsResults = ({ podcastsQuery, queryReference }: Props) => {
  const query = usePreloadedQuery<PodcastsQuery>(podcastsQuery, queryReference);
  
  const { data, loadNext, isLoadingNext } = usePaginationFragment<
    PodcastsPaginationQuery,
    PodcastsResults_podcasts$key
  >(fragment, query);

  const loadMore = useCallback(() => {
    if (isLoadingNext) return;
    loadNext(20);
  }, [isLoadingNext, loadNext]);

  return (
    <VStack
      divider={<StackDivider borderColor="gray.200" />}
      spacing={4}
      align="stretch"
      w="800px"
      h="100%"
      margin="0 auto"
      pt={3}
      pb={3}
    >
      {data.podcasts.edges.map(({ node }: any) => <PodcastItem node={node} />)}
      <Button type="button" width="120px" onClick={loadMore} justifySelf="center" alignSelf="center" isLoading={isLoadingNext}>Load more</Button>
    </VStack>
  )
};
export default PodcastsResults;

All we have to do now is run the Relay script for converting our GraphQL literals into generated files. We have now a very performant and scalable example app using Relay.

Conclusion

Relay was created with scalability and performance in mind. It’s an opinionated, robust framework that helps you to write type-safe apps. Relay is the best GraphQL client if you want to have a very robust and scalable app that will stay performant.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.