Using Apollo Client to Manage GraphQL Data in our Next.js Application

WEB
8 min read

DoltHub is a place on the internet to share, discover, and collaborate on Dolt databases. We have a series about how we built DoltHub, which goes deeper into our system and front-end architecture. If you're curious about our architecture or the reasons surrounding our decision to switch to GraphQL, I'd recommend starting with these blogs:

This blog will dive deeper into how we manage our GraphQL data with Apollo Client for our Next.js DoltHub application, which is written in Typescript and React.

Setting up Apollo Client

We use Apollo Client to manage our local and remote data with GraphQL. Its built-in integration with React, declarative data fetching, and other features make Apollo an ideal choice for our front-end state management.

Our front-end consists of multiple packages within a monorepo, managed by Yarn workspaces. The relevant applications mentioned within this blog are our Next.js dolthub and NestJS graphql-server applications.

First, we set up our Apollo config, which defines our client (dolthub) and service (graphql-server) projects.

// apollo.config.js
const path = require("path");

module.exports = {
  client: {
    includes: [
      "./components/**/*.ts*",
      "./contexts/**/*.ts*",
      "./hooks/**/*.ts*",
      "./lib/**/*.ts*",
      "./pages/**/*.ts*",
    ],
    service: {
      name: "graphql-server",
      localSchemaFile: path.resolve(__dirname, "../graphql-server/schema.gql"),
    },
  },
};

Next, we create our Apollo client. We have some helper functions in lib/apollo.tsx (view gist here), which includes some customizations for a dynamic GraphQL endpoint and server-side rendering. This article provides more information about setting up a Next.js app with Apollo.

We then use the withApollo function to pass our Apollo client instance to different pages within our pages/_app.tsx entry page component (view gist here).

We can run our Next.js dolthub server against either our deployed or local GraphQL server endpoints. Running dolthub against our local GraphQL server is especially useful when making changes to our GraphQL application, as we can see the schema changes reflected immediately in the UI.

// package.json
{
  "scripts": {
    "dev": "next dev",
    "dev:local-graphql": "GRAPHQLAPI_URL=\"http://localhost:9000/graphql\" next dev"
  }
}

Using GraphQL Code Generator

We use GraphQL Code Generator to generate types and other code from our GraphQL schema. Before GraphQL Code Generator, we were writing out all our query types in Typescript by hand, which was time-consuming, difficult to maintain, and prone to errors. GraphQL Code Generator has been a great solution for converting our GraphQL schema to Typescript.

Here's what our codegen config looks like:

## codegen.yml
overwrite: true
schema: "http://localhost:9000/graphql"
documents: "{components,contexts,hooks,lib,pages}/**/*.{ts,tsx}"
generates:
  gen/graphql-types.tsx:
    config:
      dedupeOperationSuffix: true
      withHooks: true
      withComponent: false
      withHOC: false
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
  gen/fragmentTypes.json:
    plugins:
      - "fragment-matcher"
    config:
      apolloClientVersion: 3

We have two scripts in our package.json to generate our GraphQL schema types in Typescript. The first generates types once and the latter watches for changes in our GraphQL queries as we develop. These both use our local GraphQL endpoint, so our GraphQL server must be running for them to work.

// package.json
{
  "scripts": {
    "generate-types": "graphql-codegen --config codegen.yml",
    "watch-queries": "graphql-codegen --config codegen.yml --watch"
  }
}

When we run either of the above commands, our GraphQL queries and mutations wrapped in gql tags are converted to Typescript and output to our gen/graphql-types.tsx file to be used in our components.

You can see our codegen.yml also includes an output file called gen/fragmentTypes.json. This fragment-matcher plugin generates an introspection file, but only with GraphQL unions and interfaces. This is used for our models that include unions, such as PullDetails for the pull request page, which consists of a union of pull logs, commits, and comments.

Using queries in our React components

Now that we have our Apollo client set up and query types generated, we can use GraphQL queries within our React components. To illustrate how this all comes together, I'll use one of our components called UserLinkWithProfPic. It queries for a user's information based on a username variable and renders their profile picture with a link to the user's profile. It looks like this on our website:

UserLinkWithProfPic

These are the steps we go through when we create a new component with a query.

1. Define the query

First, we need to define the shape of query we'll use to fetch user's information. If you need a reminder, this is what our models and resolvers in our GraphQL server look like. We use the gql template literal tag to wrap the GraphQL schema definitions. It looks like this:

// components/UserLinkWithProfPic/queries.ts
import { gql } from "@apollo/client";

export const USER_WITH_PROF_PIC = gql`
  query UserForUserLinkWithProfPic($username: String!) {
    user(username: $username) {
      _id
      username
      displayName
      profPic {
        _id
        url
      }
    }
  }
`;

This queries for a user and returns the fields we need from the user and profile picture models.

2. Generate types

Next, we need to generate the Typescript code for this query so that we can use it within our Typescript React component. Once we have our GraphQL server running locally, we run yarn generate-types. The resulting code from this query can be found in our gen/graphql-types.tsx file. It looks like this:

// gen/graphql.tsx
export type UserForUserLinkWithProfPicQueryVariables = Exact<{
  username: Scalars["String"];
}>;

export type UserForUserLinkWithProfPicQuery = { __typename?: "Query" } & {
  user: { __typename?: 'User' }
  & Pick<User, '_id' | 'username' | 'displayName'>
  & { profPic: (
    { __typename?: "ProfilePicture" } & Pick<
      ProfilePicture,
      "_id" | "url"
    >;
  ) };
};

export const UserForUserLinkWithProfPicDocument = gql`
  query UserForUserLinkWithProfPic($username: String!) {
    user(username: $username) {
      _id
      username
      displayName
      profPic {
        _id
        url
      }
    }
  }
`;

/**
 * __useUserForUserLinkWithProfPicQuery__
 *
 * To run a query within a React component, call `useUserForUserLinkWithProfPicQuery` and pass it any options that fit your needs.
 * When your component renders, `useUserForUserLinkWithProfPicQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useUserForUserLinkWithProfPicQuery({
 *   variables: {
 *      username: // value for 'username'
 *   },
 * });
 */
export function useUserForUserLinkWithProfPicQuery(
  baseOptions: Apollo.QueryHookOptions<UserForUserLinkWithProfPicQuery, UserForUserLinkWithProfPicQueryVariables>
) {
  return Apollo.useQuery<UserForUserLinkWithProfPicQuery, UserForUserLinkWithProfPicQueryVariables>(UserForUserLinkWithProfPicDocument, baseOptions);
}

DoltHub currently consists of over 230 queries, so you can see how much of a pain it would be to write out this code by hand for every query. Now we have some Typescript types for this query, as well as an Apollo query hook, we can use this query to render a React component.

3. Use the query in component

When we're creating a component that renders based on the results of a query, we use an "inner/outer" pattern, where an "outer" component handles query loading and error states and an "inner" component renders the data when the query is successful.

For UserLinkWithProfPic, the "outer" component will look something like this:

// components/UserLinkWithProfPic/index.tsx
import React from "react";
import ReactLoader from "react-loader";
import { useUserForProfPicQuery } from "../../gen/graphql-types";
import ErrorMsg from "../ErrorMsg";
import Inner from "./Inner";

type Props = {
  username: string;
};

export default function UserLinkWithProfPic(props: Props) {
  const { data, loading, error } = useUserForProfPicQuery({
    variables: { username: props.username },
  });

  // Handles loading state using spinner
  if (loading) return <ReactLoader loaded={false} />;

  // Displays error
  if (error) return <ErrorMsg err={error} />;

  // Renders inner component with data
  return <Inner data={data} />;
}

And then our "inner" component can render the user data we want to display:

// components/UserLinkWithProfPic/Inner.tsx
import React from "react";
import { UserForUserLinkWithProfPicQuery } from "../../gen/graphql-types";
import UserLink from "../links/UserLink";
import css from "./index.module.css";

type Props = {
  data: UserForUserLinkWithProfPicQuery;
};

export default function Inner({ data }: Props) {
  return (
    <span>
      <img src={data.user.profPic.url} className={css.profPic} alt="" />
      <UserLink params={{ username: data.user.username }}>
        {data.user.displayName}
      </UserLink>
    </span>
  );
}

Now UserLinkWithProfPic is ready to use within our other components!

Refactoring to fragments

Fragments are useful for defining data you may want to reuse in multiple places or for keeping components independent of specific queries. In this case, we could refactor our query to use both a UserWithProfPic fragment (based on our User model) and ProfPic fragment (based on our ProfilePicture model).

// components/UserLinkWithProfPic/queries.ts
import { gql } from "@apollo/client";

export const USER_WITH_PROF_PIC = gql`
  fragment ProfPic on ProfilePicture {
    _id
    url
  }
  fragment UserWithProfPic on User {
    _id
    username
    displayName
    profPic {
      ...ProfPic
    }
  }
  query UserForUserLinkWithProfPic($username: String!) {
    user(username: $username) {
      ...UserWithProfPic
    }
  }
`;

Once generated, the fragment types will look like this:

// gen/graphql.tsx
export type ProfPicFragment = { __typename?: "ProfilePicture" } & Pick<
  ProfilePicture,
  "_id" | "url"
>;

export type UserWithProfPicFragment = { __typename?: "User" } & Pick<
  User,
  "_id" | "username" | "displayName"
> & { profPic: { __typename?: "ProfilePicture" } & ProfPicFragment };

And our UserForUserLinkWithProfPicQuery will be refactored to use the fragments:

// gen/graphql.tsx
export type UserForUserLinkWithProfPicQuery = { __typename?: "Query" } & {
  user: { __typename?: "User" } & UserWithProfPicFragment;
};

Our Inner component can now expect a prop called user with type UserWithProfPicFragment, and our data would be passed down to Inner like this:

// components/UserLinkWithProfPic/index.tsx
return <Inner user={data.user} />;

We could also make a reusable profile picture component using the ProfPicFragment type. It would look like:

// components/ProfilePicture/index.tsx
import React from "react";
import { ProfPicFragment } from "../../gen/graphql-types";
import css from "./index.module.css";

type Props = {
  profPic: ProfPicFragment;
};

export default function ProfilePicture({ profPic }: Props) {
  return <img src={profPic.url} className={css.profPic} alt="" />;
}

This component can now be reused in other components with different query shapes, such as queries for organization or team profile pictures.

TL;DR

Apollo Client helps us manage our local and remote data with GraphQL. Not only can we automatically track loading and error states with declarative data fetching, but we can take advantage of the latest React features, such as hooks, from its built-in React integration. This and Apollo's other features make our code predictable and intuitive to learn.

While Apollo Client provides tools for Typescript, manually writing out the types for each query, variable, and hook is time-consuming, error-prone, and difficult to maintain. GraphQL Code Generator takes our GraphQL schema and converts everything we need to Typescript with a simple command. We use these generated type definitions to fetch data with hooks using less code, enforce types in component props and React unit test mocks, and refetch queries when data is mutated.

We use Typescript for everything, and while setting up and using Apollo Client for our Next.js DoltHub application requires a few extra steps, we think the upsides of a typed language are worth it.

Have any questions, comments, or ideas? Come chat with us in our Discord server.

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.