Dwarves
Memo
Type ESC to close search bar

A Fragment Colocation Pattern with React & Apollo GraphQL

When working with complex GraphQL schemas, it’s common to have shared fields across different types. A fragment colocation pattern allows us to define fragments alongside their corresponding components, resulting in a more cohesive and maintainable codebase.

By colocating fragments, we can easily reuse them across components that share common fields, reducing redundant code and promoting consistency. This can be further enhanced by using other layers of tooling, i.e. converting fragments into Typescript interfaces or auto generating React hooks for queries & mutations.

This note aims to discuss such a pattern, made possible with:

First, let’s step back & take a quick look at what is a fragment.

Fragments

GraphQL fragment is a piece of logic that can be shared between multiple queries and mutations.

Here’s the declaration of a NameParts fragment that can be used with any Person object:

fragment NameParts on Person {
  firstName
  lastName
}

Every fragment includes a subset of the fields that belong to its associated type. In the above example, the Person type must declare firstName and lastName fields for the NameParts fragment to be valid.

We can now include the NameParts fragment in any number of queries and mutations that refer to Person objects, like so:

query GetPerson {
  people(id: "7") {
    ...NameParts
    avatar(size: LARGE)
  }
}

You precede an included fragment with three periods (...), much like JavaScript spread syntax.

Based on our NameParts definition, the above query is equivalent to:

query GetPerson {
  people(id: "7") {
    firstName
    lastName
    avatar(size: LARGE)
  }
}

If we later change which fields are included in the NameParts fragment, we automatically change which fields are included in operations that use the fragment. This reduces the effort required to keep fields consistent across a set of operations.

That’s it for fragment. Let’s move on to how we actually implement a colocation pattern with fragments and React.

Example: Animal Cards and Lists

Let’s consider an example where we’re building an application that showcases cats and dogs. We want to implement reusable components to display individual animal cards (CatCard and DogCard) as well as lists of animals (CatList and DogList).

For backend, let’s say we are using [NestJS](nestjs-a-progressive-node.js framework](https:/nestjs.com). The schema consists of the following types:

interface AnimalModel {
  id: string
  name: string
  bread: string
}

interface CatModel extends AnimalModel {
  age: number
}

interface DogModel extends AnimalModel {
  weight: number
}

We’ll define fragments for the shared fields (id, name, and breed) within the AnimalModel type, and define two other fragments for cat & dog that extend from the animal fragment:

import { gql } from '@apollo/client'

const ANIMAL_FRAGMENT = gql`
  fragment AnimalFragment on AnimalModel {
    id
    name
    breed
  }
`

const CAT_FRAGMENT = gql`
  fragment CatFragment on CatModel {
    ...AnimalFragment
    age
  }
  ${ANIMAL_FRAGMENT}
`

const DOG_FRAGMENT = gql`
  fragment DogFragment on DogModel {
    ...AnimalFragment
    weight
  }
  ${ANIMAL_FRAGMENT}
`

By this point, we are still missing something until we can build the CatCard and DogCard components - the Typescript types.

With @graphql-codegen/cli, we can convert these fragments into Typescript interfaces by running a CLI script. I will not go into details into how the tool work so you should also give this link](https://the-guild.dev/graphql/codegen)) a look - they provide an interactive example.

Basically @graphql-codegen/cli will:

After running the CLI, the typing output will look like below:

// Output file: graphql/generated.ts

export type AnimalFragment {
	__typename?: 'AnimalModel';
	id: Scalars['String'];
	name: Scalars['String'];
	bread: Scalars['String'];
}

export type CatFragment {
	__typename?: 'CatModel';
	id: Scalars['String'];
	name: Scalars['String'];
	bread: Scalars['String'];
	age: Scalars['Int'];
}

export type DogFragment {
	__typename?: 'DogModel';
	id: Scalars['String'];
	name: Scalars['String'];
	bread: Scalars['String'];
	weight: Scalars['Int'];
}

Now that we have everything we need, let’s build the CatCard and DogCard components:

import { CatFragment } from 'graphql/generated'

// const ANIMAL_FRAGMENT = gql`...`

// const CAT_FRAGMENT = gql`...`

// const DOG_FRAGMENT = gql`...`

const CatCard = (props: { cat: CatFragment }) => {
  const { cat } = props

  // Component rendering logic
}

const DogCard = (props: { cat: DogFragment }) => {
  const { cat } = props

  // Component rendering logic
}

The properties cat and dog will have the types we have defined for the fragments they are actually using - an exact map from GraphQL models to Typescript types that we can be sure will always be accurate as long as the fragments we define match the schema from GraphQL backend.

Next, let’s build the CatList and DogList component and see how we handle queries. Let’s defined 2 queries to get cats and dogs:

// ... import needed stuff
import { gql } from '@apollo/client'

gql`
  query GetCatList {
    cats {
      ...CatFragment
    }
  }
  ${CAT_FRAGMENT}
`

gql`
  query GetDogList {
    dogs {
      ...DogFragment
    }
  }
  ${DOG_FRAGMENT}
`

Then we run @graphql-codegen/cli again. Depending on how we set-up the CLI, output will vary so the below are what I normally work with:

Core features of Apollo such as request state management, caching & revalidating are all functional through these custom hooks.

Now that we have the queries, let’s build the CatList and DogList components:

// ... import needed stuff
import { useGetCatListQuery, useGetDogListQueryLazy } from 'graphql/generated';

const CatList = () = {
	const data = useGetCatListQuery();
	const cats = data.data?.cats || [];

	if (data.loading) {
		return null;
	}

	return cats.map(cat => <CatCard cat={cat} />);
}

const DogList = () = {
	const [getDogList] = useGetDogListQueryLazy();
	const [dogs, setDogs] = useState<DogFragment[]>([]);

	useEffect(() => {
		getDogList().then(res => setDogs(res.data?.dogs || []));
	}, [])

	return dogs.map(dog => <DogCard dog={dog} />);
}

In the above code, the CatList and DogList are using the query hooks generated in the previous step. CatList and DogList are using CatCard and DogCard components, while the queries are using CatFragment and DogFragment defined together with the card components.

All the types match perfectly.

The Benefits

This pattern offers several benefits:

The Disadvantages

While the Fragment Colocation Pattern provides several advantages, it’s important to consider its limitations:

Conclusion

The pattern we have discussed provides an effective way to colocate fragments with their corresponding components, while also provides strict typing and other quality-of-life features with extra toolings.

This pattern enhances code reusability, consistency, and readability. By reusing fragments across components that share common fields, we can avoid duplication and ensure a more maintainable codebase.

On the other hand, we also need to keep in mind its limitations, such as potential fragment duplication and increased complexity with larger codebases, and a steep learning curve.

All in all, personally I think this a pattern that’s easy to adopt, hard to master (thus also easy to mess up). It’s true to the sprit of GraphQL, and worth a try to see for ourselves how it can give us a different approach to building optimized, well-organized code-bases.