Skip to content

Fragment Colocation

When presenting GraphQL, its features often turn into a box-ticking exercise of comparing it to alternative solutions of server-client API design, until we may ask ourselves whether GraphQL’s strengths mostly lie in bringing a community together with clever decisions we can now all agree and rely on…

However, while some of what makes GraphQL great is that many of its core principles aren’t new ideas, its less talked about strength lies in fragment composition and hierarchical schema design, which matches our data needs for componentized apps.

Introduction to Fragments

In GraphQL, fragments have many uses, and the uses of “Fragment Colocation” are basically a combination of many of the other uses for fragments.

Reusing Selection Sets

At their most fundamental, fragments allow us to define a selection set and reuse this set in multiple places of our GraphQL document.

import { graphql } from 'gql.tada';
const query = graphql(`
query PostsOverview {
latestPosts { ...PostCard }
trendingPosts { ...PostCard }
}
fragment PostCard on Post {
id, text, createdAt
}
`);

In the prior example, we’ve extracted two selection sets into a PostCard fragment. When a query we’re writing uses the same data in multiple code paths, we may use fragments to only write a re-used selection set once.

Type Conditions

Fragments are also used whenever we’re trying to specify that a certain selection set only applies to one possible type of an abstract type, like a union or interface.

import { graphql } from 'gql.tada';
const query = graphql(`
query PostsOverview {
latestPosts {
id
...MediaCard on MediaPost {
videoUrl
}
...TextCard on TextPost {
text
}
}
}
`);

The above example shows a query for a schema where latestPosts exposes an interface that is implemented by two types; MediaPost and TextPost.

We may use fragments to conditionally apply a selection set to either of these types, which is like “Type Narrowing” in GraphQL.

@include & @skip Conditions

GraphQL also features two built-in directives, @include and @skip, which we can use to conditionally include a fragment, based on a variable we pass to our query.

import { graphql } from 'gql.tada';
const query = graphql(`
query PostsOverview($showDetails: Boolean!) {
latestPosts {
id
text
...PostDetails @include(if: $showDetails)
}
}
fragment PostDetails on Post {
id
author { name }
location { city }
}
`);

Here, we only include a PostDetails fragment if $showDetails is set, which means, fragments also allow us to alter the query based on some input variables. We can use this to slightly alter the result shape based on what components we know we’ll render, while keeping the query itself the same.

Fragment Colocation

All the above examples of how we can use fragments may feel vaguely familiar to us, even if this is the first time we’re seeing fragments in action. That might be because fragments are structured very similarly to how components in componentized apps work.

While querying fields is similar to how we access data in front-end code, and hence map the hierarchy of data we need; Fragments are similar to how we may structure components.

AuthorLabel.tsx
import { FragmentOf, graphql } from 'gql.tada';
export const authorLabelFragment = graphql(`
fragment AuthorLabel on Author @_unmask {
id
handle
name
}
`);
export const AuthorLabel = (props: {
data: FragmentOf<typeof authorLabelFragment>
}) => {
const { data } = props;
return (
<span>
{`by ${data.name}`}
<em>@{data.handle}</em>
</span>
);
};

With fragments, like our authorLabelFragment above, we can define the data a component requires to render right next to the component itself, which keeps concerns on how to fetch this data away from our presentational components, while still defining what data the component requires.

Nested Fragment Composition

While colocating fragments is interesting on its own, it really becomes useful once we define more nested components, and compose their fragments.

Let’s create a PostCard component that renders the AuthorLabel component we’ve already defined above:

PostCard.tsx
import { FragmentOf, graphql } from 'gql.tada';
import { authorLabelFragment, AuthorLabel } from './AuthorLabel';
export const postCardFragment = graphql(`
fragment PostCard on Post @_unmask {
id
text
author { ...AuthorLabel }
}
`, [authorLabelFragment]);
export const PostCard = (props: {
data: FragmentOf<typeof postCardFragment>
}) => {
const { data } = props;
return (
<section>
<p>{data.text}</p>
<AuthorLabel data={data.author} />
</section>
);
};

As we can see, defining reusing and composing fragments, is just as easy as reusing and composing components.

No matter whether where we’re using the PostCard or AuthorLabel components, as long as we compose fragments upwards, we’ll eventually be able to compose them into a query, at the level of our screen’s code, and hence combine the data requirements of all of our components.

Fragment Masking

In the previous examples, you may have noticed the @_unmask directive.

In gql.tada, a technique called “Fragment Masking” is applied to the generated types of your fragments, and @_unmask disables this for the purpose of our example code. Fragment Masking hides the types of a fragment on the fragment’s derived type. This prevents leaking data when composing fragments.

Let’s consider what happens if PostCard started to accidentally depend on data that only the AuthorLabel’s fragment defines.

PostCard.tsx
export const PostCard = (props: {
data: FragmentOf<typeof postCardFragment>
}) => {
const { data } = props;
return (
<section>
<p>{data.text}</p>
<span>{data.author.name}</span>
<AuthorLabel data={data.author} />
</section>
);
};

We can fix this by removing @_unmask on the AuthorLabel component to re-enable fragment masking. This will effectively “hide” the authorLabelFragments’s data from the PostCard component to keep the fragments isolated from one another on a type-level.

PostCard.tsx
import { FragmentOf, graphql, readFragment } from 'gql.tada';
export const authorLabelFragment = graphql(`
fragment AuthorLabel on Author @_unmask {
fragment AuthorLabel on Author {
id
handle
name
}
`);
export const AuthorLabel = (props: {
data: FragmentOf<typeof authorLabelFragment>
}) => {
const { data } = props;
const data = readFragment(authorLabelFragment, props.data);
return (
<span>
{`by ${data.name}`}
<em>@{data.handle}</em>
</span>
);
};

Inside the inferred TypeScript types, when fragment masking isn’t disabled using @_unmask, then gql.tada will infer masked types. In TypeScript, the type that FragmentOf<> returns may look like the following:

// FragmentType<typeof authorLabelFragment> with @_unmask:
type unmaskedAuthor = {
id: string | number;
handle: string | null;
name: string | null;
};
// FragmentType<typeof authorLabelFragment> without @_unmask:
type maskedAuthor = {
[$tada.fragmentRefs]: {
AuthorLabel: $tada.ref;
};
};