Skip to main content

Command Palette

Search for a command to run...

Improving Code Readability Using GraphQL Fragment Colocation

Updated
4 min read
Improving Code Readability Using GraphQL Fragment Colocation

TL;DR

It will eliminate over-fetching, improve readability, and make it easier to maintain
It ensures that only necessary data is queried and that your code remains modular and efficient

Background

At my job, I worked on a Next.js application that used GraphQL. We also used GraphQL codegen for TypeScript types.

Initially, a senior developer with a mobile development background introduced a module-based pattern, which led us to create models for each GraphQL type.

The Initial Approach: Using Models

This is a simplified version of the UserModel:

export class UserModel implements User {
  public readonly id: Scalars['ID'];
  public readonly firstName: Scalars['String'];
  public readonly lastName: Scalars['String'];

  constructor(user: User | undefined) {
    this.id = user?.id ?? '';
    this.firstName = user?.firstName ?? '';
    this.lastName = user?.lastName ?? '';
  }

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

This UserModel implements auto generated User type. (So eventually includes all user tables fields.) And defined some private methods.

And for the GraphQL, we kept queries in a single folder:

// src/graphql/queries/user.graphql

query User {
  user {
    ...UserDetails
  }
}

fragment UserDetails {
  id
  firstName
  lastName
  email
  address
  phoneNumber
  # Fetching all fields
}

We were using fragments but read all fields to make it easier to reference them in the models.

And this is how we refer to user’s full name:

const user = new UserModel(graphql_result);
user.fullName();

For this example, using only firstName and lastName is straightforward. However, in real applications, models tend to be more complex.

Since we were querying all fields and including them in the models, we were unsure which properties were necessary in different parts of the app. As a result, we often ended up over-fetching data, which negated the benefits of GraphQL. 🫣

The Shift: Seeking a better approach

After a few years, the original project was canceled. However, I got the opportunity to build a new app from scratch with the same tech stack. The senior developer had left, and I became the lead front-end developer. 😎 This time, I decided to get rid of the module-based pattern entirely.

Initially, I wrote helper functions:

// src/helpers/user.ts

const getFullName = (user: User) => {
  return `${user.firstName} ${user.lastName}`;
};

However, I still kept using the generated User type. Also, GraphQL queries are still in a single folder, leading to the same inefficiencies.

Then, I got a advisor who introduced me to the power of GraphQL fragment colocation. ✨

What is GraphQL Fragment Colocation?

GraphQL fragment colocation is the practice of defining fragments near the components or functions that consume them. Instead of keeping all GraphQL queries in a centralized location, colocating fragments ensures that only the necessary fields are queried and makes the code more modular and maintainable.

For more details, check out:

Implementing Colocation: A New Approach

We decided on new rules based on my advisor’s advice.

New Coding Rules are:

  • Write fragments in the same file as the functions/components using them

  • Query only the fields needed in that file

  • Avoid using generated types directly

Example:

// src/helpers/user.tsx
export const USER_FULLNAME = graphql(`
  fragment UserFullName on User {
    firstName
    lastName
    middleName
  }
`);

export const getFullName = (user: {
  firstName: string;
  lastName: string;
  middleName: string;
}) => {
  return `${user.firstName} ${user.middleName} ${user.lastName}`;
};

Now, the helper function defines its required fields explicitly via a fragment. This makes it clear which properties are used and avoids unnecessary data fetching.

Or we could use DocumentType which is generated from codegen:

import type { DocumentType } from "~/__generated__";

export const getFullName = (user: DocumentType<typeof USER_FULLNAME>) => {
  return `${user.firstName} ${user.middleName} ${user.lastName}`;
};

* Note that we are not using User type here.

When calling the function, we ensure the required data is passed:

// src/page/user.tsx
import { getFullName } from '../helpers/user';

const { data } = useQuery(
  graphql(`
    query UserFullNameQuery {
      user {
        ...UserFullName
      }
    }
  `)
);

const fullName = getFullName(data.user);

Conclusion

By colocating GraphQL fragments and only fetching necessary fields, we achieve:

  • Improved Readability: Queries are easier to understand and maintain.

  • Reduced Over-fetching: We fetch only the data we actually use.

  • Better Maintainability: Changes to data structures remain localized.

This approach makes GraphQL development cleaner and more efficient, ensuring that our frontend remains both performant and easy to work with. 🥳