Using Apollo Client to Manage GraphQL Data in our Next.js Application
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:
- Our front-end architecture
- Why we switched from gRPC to GraphQL
- Delivering declarative data with GraphQL
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:
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.