Skip to content

Commit

Permalink
Add Fragment Colocation page content
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten committed Jan 24, 2024
1 parent 2160dec commit 55ca95c
Show file tree
Hide file tree
Showing 2 changed files with 289 additions and 1 deletion.
2 changes: 1 addition & 1 deletion website/src/content/docs/get-started/writing-graphql.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ const result: ResultOf<typeof TodosQuery> = {
{
id: 'ExampleID',
[$tada.fragmentRefs]: {
TodoItem: unique symbol;
TodoItem: $tada.ref;
};
},
]
Expand Down
288 changes: 288 additions & 0 deletions website/src/content/docs/guides/fragment-colocation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,291 @@
title: Fragment Colocation
description: How GraphQL fragments are effectively used in componentized apps.
---

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.

```ts
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`.

```ts
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.

:::note
The above example uses an inline fragment spread, however, the
same principle of type conditions applies to regular fragments
and fragment spreads.
:::

### `@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.

```ts
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.

```tsx title="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:

```tsx title="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.

```tsx title="PostCard.tsx" ins={"PostCard now accidentally depends on AuthorLabel’s data:":8-9}
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.

```tsx title="PostCard.tsx" {"Removing @_unmask isolates this fragment’s data.":4-6} del={5} ins={6} ins={"We now have to add readFragment() to unwrap the masked fragment:":16-18} del={17} ins={18}
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>
);
};
```

:::note
This is the default behaviour in `gql.tada`, and happens unless you add `@_unmask`
to a fragment.

However, by default, we recommend you not to disable Fragment Masking unless you
absolutely have to, to enforce fragment composition.
:::


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:

```ts
// 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;
};
};
```


0 comments on commit 55ca95c

Please sign in to comment.