Fragments

Share fields between operations


A GraphQL fragment is a set of fields you can reuse across multiple queries and mutations. Fragments are especially useful when colocated with components to define the component's data requirements.

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

GraphQL
1fragment NameParts on Person {
2  firstName
3  lastName
4}

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.

You can include the NameParts fragment in any number of operations that refer to Person objects by using the spread operator (...), followed by the fragment name:

GraphQL
1query GetPerson {
2  people(id: "7") {
3    ...NameParts
4    avatar(size: LARGE)
5  }
6}

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

GraphQL
1query GetPerson {
2  people(id: "7") {
3    firstName
4    lastName
5    avatar(size: LARGE)
6  }
7}

Changes to the NameParts fragment automatically update the fields included in any operations that use it. This reduces the effort required to keep fields consistent across a set of operations.

Example usage

Let's say we have a blog application that executes several GraphQL operations related to comments (submitting a comment, fetching a post's comments, etc.). Our application likely has a Comment component that is responsible for rendering comment data.

We can define a fragment on the Comment type to define the Comment component's data requirements, like so:

JavaScript
Comment.js
1import { gql } from '@apollo/client';
2
3export const COMMENT_FRAGMENT = gql`
4  fragment CommentFragment on Comment {
5    id
6    postedBy {
7      username
8      displayName
9    }
10    createdAt
11    content
12  }
13`;

The example above exports the fragment from the Comment.js component file. You can declare fragments in any file of your application, though we recommend this approach of colocating fragments with your components.

We can then include the CommentFragment fragment in a GraphQL operation like so:

JavaScript
PostDetails.jsx
1import { gql } from '@apollo/client';
2import { COMMENT_FRAGMENT } from './Comment';
3
4const GET_POST_DETAILS = gql`
5  query GetPostDetails($postId: ID!) {
6    post(postId: $postId) {
7      title
8      body
9      author
10      comments {
11        ...CommentFragment
12      }
13    }
14  }
15
16  ${COMMENT_FRAGMENT}
17`;
18
19// ...PostDetails component definition...
  • We first import COMMENT_FRAGMENT because it's declared in another file.

  • We add our fragment definition to the GET_POST_DETAILS gql template literal via a placeholder (${COMMENT_FRAGMENT})

  • We include the CommentFragment fragment in our query with standard ... notation.

Registering named fragments using createFragmentRegistrySince 3.7.0

Registering fragments with your InMemoryCache instance lets you refer to them by name in queries and cache operations (for example, cache.readFragment, cache.readQuery, and cache.watch) without needing to interpolate their declarations.

note
We do not recommend using the fragment registry when using the graphql function generated by the GraphQL Codegen client preset. The client preset creates precompiled GraphQL documents that already include fragment definitions.

Let's look at an example in React.

JavaScript
index.js
1import { ApolloClient, gql, InMemoryCache } from "@apollo/client";
2import { createFragmentRegistry } from "@apollo/client/cache";
3
4const client = new ApolloClient({
5  uri: "http://localhost:4000/graphql",
6  cache: new InMemoryCache({
7    fragments: createFragmentRegistry(gql`
8      fragment ItemFragment on Item {
9        id
10        text
11      }
12    `)
13  })
14});

Since ItemFragment was registered with InMemoryCache, it can be referenced by name, as seen below, with the fragment spread inside of the GetItemList query.

JavaScript
ItemList.jsx
1const listQuery = gql`
2  query GetItemList {
3    list {
4      ...ItemFragment
5    }
6  }
7`;
8function ToDoList() {
9  const { data } = useQuery(listQuery);
10  return (
11    <ol>
12      {data?.list.map(item => (
13        <Item key={item.id} text={item.text} />
14      ))}
15    </ol>
16  );
17}

Overriding registered fragments with local versions

Queries can declare their own local versions of named fragments which take precedence over ones registered via createFragmentRegistry, even if the local fragment is only indirectly referenced by other registered fragments. Take the following example:

JavaScript
index.js
1import { ApolloClient, gql, InMemoryCache } from "@apollo/client";
2import { createFragmentRegistry } from "@apollo/client/cache";
3
4const client = new ApolloClient({
5  uri: "http://localhost:4000/graphql",
6  cache: new InMemoryCache({
7    fragments: createFragmentRegistry(gql`
8      fragment ItemFragment on Item {
9        id
10        text
11        ...ExtraFields
12      }
13
14      fragment ExtraFields on Item {
15        isCompleted
16      }
17    `)
18  })
19});

The local version of the ExtraFields fragment declared in ItemList.jsx takes precedence over the ExtraFields fragment originally registered with the InMemoryCache instance. Thus, the local definition will only be used when GetItemList query is executed, because explicit definitions take precedence over registered fragments.

JavaScript
ItemList.jsx
1const GET_ITEM_LIST = gql`
2  query GetItemList {
3    list {
4      ...ItemFragment
5    }
6  }
7
8  fragment ExtraFields on Item {
9    createdBy
10  }
11`;
12function ToDoList() {
13  const { data } = useQuery(GET_ITEM_LIST);
14  return (
15    <ol>
16      {data?.list.map((item) => (
17        {/* `createdBy` exists on the returned items, `isCompleted` does not */}
18        <Item key={item.id} text={item.text} author={item.createdBy} />
19      ))}
20    </ol>
21  );
22}

Lazily registering named fragments

Fragments don't need to be defined upfront when the cache is created. Instead, you can register named fragments lazily with the fragment registry. This is especially useful when combined with colocated fragments whose fragment definitions are defined in component files. Let's look at an example:

JavaScript
fragmentRegistry.js
1export const { fragmentRegistry } = createFragmentRegistry();
JavaScript
index.js
1import { fragmentRegistry } from "./fragmentRegistry";
2
3const client = new ApolloClient({
4  uri: "http://localhost:4000/graphql",
5  cache: new InMemoryCache({
6    fragments: fragmentRegistry,
7  })
8});

We create a separate file that creates and exports our fragment registry. This lets us access our shared fragment registry across our application. We use this shared fragment registry with our InMemoryCache instance.

JavaScript
TodoItem.jsx
1import { gql } from "@apollo/client";
2import { fragmentRegistry } from "./fragmentRegistry";
3
4// Define the fragment outside the component to ensure it gets registered when this module is loaded.
5const ITEM_FRAGMENT = gql`
6  fragment ItemFragment on Item {
7    # ...
8  }
9`
10
11fragmentRegistry.register(ITEM_FRAGMENT);
12
13function TodoItem() {
14  // ...
15}

We then import our shared fragment registry into our component file and register our fragment definition.

caution
You need to register fragment definitions with the fragment registry before executing operations that use them. This can be problematic when lazy loading component files because the application might not register the fragment definition with the registery until after the query begins executing. Move the fragment definition to a shared file that isn't lazy-loaded.

Colocating fragments

The tree-like structure of a GraphQL response resembles the hierarchy of a frontend's rendered components. Because of this similarity, you can use fragments to split query logic between components, so each component requests exactly the fields it needs. This helps make your component logic more succinct by combining multiple UI components into a single data fetch.

Consider the following view hierarchy for an app:

In this app, the FeedPage component executes a query to fetch a list of FeedEntry objects. The EntryInfo and VoteButtons subcomponents need specific fields from the enclosing FeedEntry object.

Creating colocated fragments

A colocated fragment is just like any other fragment, except it's defined in the same file as a particular component that uses the fragment's fields. For example, the VoteButtons child component of FeedPage might use the fields score and vote { choice } from the FeedEntry object:

JavaScript
VoteButtons.jsx
1export const VOTE_BUTTONS_FRAGMENT = gql`
2  fragment VoteButtonsFragment on FeedEntry {
3    score
4    vote {
5      choice
6    }
7  }
8`

After you define a fragment in a child component, the parent component can refer to it in its own colocated fragments, like so:

JavaScript
FeedEntry.jsx
1export const FEED_ENTRY_FRAGMENT = gql`
2  fragment FeedEntryFragment on FeedEntry {
3    commentCount
4    repository {
5      full_name
6      html_url
7      owner {
8        avatar_url
9      }
10    }
11    ...VoteButtonsFragment
12    ...EntryInfoFragment
13  }
14  ${VOTE_BUTTONS_FRAGMENT}
15  ${ENTRY_INFO_FRAGMENT}
16`
tip
To prevent coupling with deeply nested components, we recommend parent components only add fragments defined by their directly-rendered children. In this example, our FeedPage should not use the EntryInfoFragment or VoteButtonsFragment directly. Instead the FeedPage uses the FeedEntryFragment fragment colocated with the FeedEntry component to combine the VoteButtonsFragment and EntryInfoFragment fragments into its own fragment.

There's nothing special about the naming of VoteButtonsFragment or EntryInfoFragment. We recommend prefixing the fragment name with the component name to make it easily identifiable when combined with other fragments. However any naming convention works as long as you can retrieve a component's fragments given the component.

Importing fragments when using Webpack

When loading .graphql files with graphql-tag/loader, include fragments using import statements. For example:

GraphQL
1#import "./someFragment.graphql"

This makes the contents of someFragment.graphql available to the current file. See the Webpack Fragments section for additional details.

Using fragments with unions and interfaces

You can define fragments on unions and interfaces.

Here's an example of a query that includes a shared field and two in-line fragments:

GraphQL
1query AllCharacters {
2  allCharacters {
3    name
4
5    ... on Jedi {
6      side
7    }
8
9    ... on Droid {
10      model
11    }
12  }
13}

The AllCharacters query above returns a list of Character objects. The Character type is an interface type that both the Jedi and Droid types implement. Each item in the list includes a side field if it's an object of type Jedi, and it includes a model field if it's of type Droid. Both Jedi and Droid objects include a name field.

However, for this query to work, the client needs to understand the polymorphic relationship between the Character interface and the types that implement it. To inform the client about these relationships, you must pass a possibleTypes option when you initialize your InMemoryCache instance.

Defining possibleTypes manually

Use the possibleTypes option to the InMemoryCache constructor to specify supertype-subtype relationships in your schema. This object maps the name of an interface or union type (the supertype) to the types that implement or belong to it (the subtypes).

Here's an example possibleTypes declaration:

JavaScript
1const cache = new InMemoryCache({
2  possibleTypes: {
3    Character: ["Jedi", "Droid"],
4    Test: ["PassingTest", "FailingTest", "SkippedTest"],
5    Snake: ["Viper", "Python"],
6  },
7});

This example lists three interfaces (Character, Test, and Snake) and the object types that implement them.

If your schema includes only a few unions and interfaces, you can probably specify your possibleTypes manually without issue. However, as your schema grows in size and complexity, you should instead generate possibleTypes automatically from your schema.

Generating possibleTypes automatically

The following script translates a GraphQL introspection query into a possibleTypes configuration object:

JavaScript
1const fetch = require('cross-fetch');
2const fs = require('fs');
3
4fetch(`${YOUR_API_HOST}/graphql`, {
5  method: 'POST',
6  headers: { 'Content-Type': 'application/json' },
7  body: JSON.stringify({
8    variables: {},
9    query: `
10      {
11        __schema {
12          types {
13            kind
14            name
15            possibleTypes {
16              name
17            }
18          }
19        }
20      }
21    `,
22  }),
23}).then(result => result.json())
24  .then(result => {
25    const possibleTypes = {};
26
27    result.data.__schema.types.forEach(supertype => {
28      if (supertype.possibleTypes) {
29        possibleTypes[supertype.name] =
30          supertype.possibleTypes.map(subtype => subtype.name);
31      }
32    });
33
34    fs.writeFile('./possibleTypes.json', JSON.stringify(possibleTypes), err => {
35      if (err) {
36        console.error('Error writing possibleTypes.json', err);
37      } else {
38        console.log('Fragment types successfully extracted!');
39      }
40    });
41  });

You can then import the generated possibleTypes JSON module into the file where you create your InMemoryCache:

TypeScript
1import possibleTypes from './path/to/possibleTypes.json';
2
3const cache = new InMemoryCache({
4  possibleTypes,
5});

Generating possibleTypes with GraphQL Codegen

GraphQL Codegen has the ability to generate possibleTypes for you using the fragment-matcher plugin. Follow the guide in the fragment matcher plugin docs to configure GraphQL Codegen to write a JSON file that contains possibleTypes.

You can then import the generated possibleTypes JSON module into the file where you create your InMemoryCache:

TypeScript
1import possibleTypes from './path/to/possibleTypes.json';
2
3const cache = new InMemoryCache({
4  possibleTypes,
5});

useFragmentSince 3.8.0

The useFragment hook represents a lightweight live binding into the Apollo Client Cache. It enables Apollo Client to broadcast specific fragment results to individual components. This hook returns an always-up-to-date view of whatever data the cache currently contains for a given fragment. useFragment never triggers network requests of its own.

The useQuery hook remains the primary hook responsible for querying and populating data in the cache (see the API reference). As a result, the component reading the fragment data via useFragment is still subscribed to all changes in the query data, but receives updates only when that fragment's specific data change.

note
useFragment was introduced as an experimental hook in version 3.7.0 under the named export useFragment_experimental. Starting with 3.8.0-beta.0 and greater the _experimental suffix was removed in its named export.

Example

Given the following fragment definition:

JavaScript
1const ITEM_FRAGMENT = gql`
2  fragment ItemFragment on Item {
3    text
4  }
5`;

We can first use the useQuery hook to retrieve a list of items with ids as well as any fields selected on the named ItemFragment fragment by including ItemFragment in the list field in the GetItemList query.

JavaScript
1const listQuery = gql`
2  query GetItemList {
3    list {
4      id
5      ...ItemFragment
6    }
7  }
8
9  ${ITEM_FRAGMENT}
10`;
11
12function List() {
13  const { loading, data } = useQuery(listQuery);
14
15  return (
16    <ol>
17      {data?.list.map(item => (
18        <Item key={item.id} item={item}/>
19      ))}
20    </ol>
21  );
22}
note
Instead of interpolating fragments within each query document, you can use Apollo Client's createFragmentRegistry method to pre-register named fragments with InMemoryCache. This allows Apollo Client to include the definitions for registered fragments in the document sent over the network before the request is sent. For more information, see Registering named fragments using createFragmentRegistry.

We can then use useFragment from within the <Item> component to create a live binding for each item by providing the fragment document, fragmentName and object reference via from.

TypeScript
JavaScript
1function Item(props: { id: number }) {
2  const { complete, data } = useFragment({
3    fragment: ITEM_FRAGMENT,
4    fragmentName: "ItemFragment",
5    from: {
6      __typename: "Item",
7      id: props.id
8    }
9  });
10
11  return <li>{complete ? data.text : "incomplete"}</li>;
12}
note
You may omit the fragmentName option when your fragment definition only includes a single fragment.

You may instead prefer to pass the whole item as a prop to the Item component. This makes the from option more concise.

TypeScript
JavaScript
1function Item(props: { item: { __typename: 'Item', id: number }}) {
2  const { complete, data } = useFragment({
3    fragment: ItemFragment,
4    fragmentName: "ItemFragment",
5    from: item
6  });
7
8  return <li>{complete ? data.text : "incomplete"}</li>;
9}
note
useFragment can be used in combination with the @nonreactive directive in cases where list items should react to individual cache updates without rerendering the entire list. For more information, see the @nonreactive docs.

See the API reference for more details on the supported options.

Data maskingSince 3.12.0

By default, Apollo Client returns all data for all fields defined in a GraphQL operation. As your app grows, components that query your GraphQL data can become tightly coupled to their component subtrees. Colocated fragments reduce the degree of coupling by moving components' data requirements into fragments. However, colocating fragments doesn't eliminate the issue.

Let's take a look at an example. The following Posts.jsx defines a Posts component that fetches and displays a list of posts, optionally filtering out unpublished ones, using a GraphQL query that includes a fragment for post details.

JavaScript
Posts.jsx
1import { POST_DETAILS_FRAGMENT } from './PostDetails';
2
3const GET_POSTS = gql`
4  query GetPosts {
5    posts {
6      id
7      ...PostDetailsFragment
8    }
9  }
10
11  ${POST_DETAILS_FRAGMENT}
12`;
13
14export default function Posts({ includeUnpublishedPosts }) {
15  const { data, loading } = useQuery(GET_POSTS);
16  const posts = data?.posts ?? [];
17
18  if (loading) {
19    return <Spinner />;
20  }
21
22  const allPosts = includeUnpublishedPosts
23    ? posts
24    : posts.filter((post) => post.publishedAt);
25
26  if (allPosts.length === 0) {
27    return <div>No posts to display</div>;
28  }
29
30  return (
31    <div>
32      {allPosts.map((post) => (
33        <PostDetails key={post.id} post={post} />
34      ))}
35    </div>
36  );
37}

The following PostDetails.jsx defines the fragment for post details and the associated UI elements.

JavaScript
PostDetails.jsx
1export const POST_DETAILS_FRAGMENT = gql`
2  fragment PostDetailsFragment on Post {
3    title
4    shortDescription
5    publishedAt
6  }
7`;
8
9export default function PostDetails({ post }) {
10  return (
11    <section>
12      <h1>{post.title}</h1>
13      <p>{post.shortDescription}</p>
14      <p>
15        {post.publishedAt ?
16          `Published: ${formatDate(post.publishedAt)}`
17        : 'Private'}
18      </p>
19    </section>
20  );
21}

The Posts component is responsible for fetching and rendering a list of posts. We loop over each post and render a PostDetails component to display details about the post. PostDetails uses a colocated fragment to define its own data requirements necessary to render post details, which is included in the GetPosts query.

When the includeUnpublishedPosts prop is false, the Posts component filters out unpublished posts from the list of all posts by checking the publishedAt property on the post object.

This strategy might work well for a while, but consider what happens when we start modifying the PostDetails component.

Suppose we've decided we no longer want to show the publish date on the list of posts and prefer to display it on individual posts. Let's modify PostDetails accordingly.

JavaScript
PostDetails.jsx
1export const POST_DETAILS_FRAGMENT = gql`
2  fragment PostDetailsFragment on Post {
3    title
4    shortDescription
5  }
6`;
7
8export default function PostDetails({ post }) {
9  return (
10    <section>
11      <h1>{post.title}</h1>
12      <p>{post.shortDescription}</p>
13    </section>
14  );
15}

We've removed the check for publishedAt since we no longer show the publish date. We've also removed the publishedAt field from the PostDetailsFragment fragment since we no longer use this field in the PostDetails component.

Uh oh, we just broke our app—the Posts component no longer shows any posts! The Posts component still depends on publishedAt, but because the field was declared in the PostDetailsFragment fragment, changes to PostDetails resulted in a subtle breakage of the Posts component.

This coupling is an example of an implicit dependency between components. As the application grows in complexity, these implicit dependencies can become more and more difficult to track. Imagine if PostDetails was a component nested much deeper in the component tree or if multiple queries used it.

Data masking helps eliminate these types of implicit dependencies by returning only the data declared by the component's query or fragment. As a result, data masking creates more loosely coupled components that are more resistant to change.

Enabling data masking

To enable data masking in Apollo Client, set the dataMasking flag in the ApolloClient constructor to true.

JavaScript
1const client = new ApolloClient({
2  dataMasking: true,
3  // ...
4});

When dataMasking is enabled, fields defined in fragments are hidden from components. This prevents the component from accessing data it didn't ask for.

Enabling data masking applies it to all operation types and all request-based APIs, such as useQuery, client.query, client.mutate, etc. Cache APIs, such as cache.readQuery and cache.readFragment are never masked.

tip
We recommend enabling the dataMasking flag immediately when creating new applications. See the section on adoption in an existing application to learn how to enable data masking in existing applications.

Let's revisit the example from the previous section.

JavaScript
Posts.jsx
1const GET_POSTS = gql`
2  query GetPosts {
3    posts {
4      id
5      ...PostDetailsFragment
6    }
7  }
8
9  ${POST_DETAILS_FRAGMENT}
10`;
11
12export default function Posts({ includeUnpublishedPosts }) {
13  const { data, loading } = useQuery(GET_POSTS);
14
15  // ...
16}

Our GetPosts query asks for the posts field along with an id for each post. All other fields are defined in PostDetailsFragment. If we were to inspect data, we'd see that the only accessible fields are those defined in the query but not the fragment.

JSON
1{
2  "posts": [
3    {
4      "__typename": "Post",
5      "id": "1"
6    },
7    {
8      "__typename": "Post",
9      "id": "2"
10    }
11  ]
12}

We can access more data by adding fields to the query. Let's fix the previous section's example by adding the publishedAt field to the GetPosts query so that the Posts component can use it.

JavaScript
Posts.jsx
1const GET_POSTS = gql`
2  query GetPosts {
3    posts {
4      id
5      publishedAt
6      ...PostDetailsFragment
7    }
8  }
9
10  ${POST_DETAILS_FRAGMENT}
11`;

Now if we inspect data, we'll see that publishedAt is available to the Posts component.

JSON
1{
2  "posts": [
3    {
4      "__typename": "Post",
5      "publishedAt": "2024-01-01",
6      "id": "1"
7    },
8    {
9      "__typename": "Post",
10      "publishedAt": null,
11      "id": "2"
12    }
13  ]
14}

Reading fragment data

Now that the GetPosts query is masked, we've introduced a problem for the PostDetails component. The post prop no longer contains the fields from the PostDetailsFragment fragment, preventing us from rendering that data.

We read the fragment data with the useFragment hook.

JavaScript
PostDetails.jsx
1function PostDetails({ post }) {
2  const { data, complete } = useFragment({
3    fragment: POST_DETAILS_FRAGMENT,
4    from: post,
5  });
6
7  // ...
8}

Now we use the data property returned from useFragment to render the details from the post.

JavaScript
PostDetails.jsx
1function PostDetails({ post }) {
2  const { data, complete } = useFragment({
3    fragment: POST_DETAILS_FRAGMENT,
4    from: post,
5  });
6
7  // It's a good idea to check the `complete` flag to ensure all data was
8  // successfully queried from the cache. This can indicate a potential
9  // issue with the cache configuration or parent object when `complete`
10  // is `false`.
11  if (!complete) {
12    return null;
13  }
14
15  return (
16    <section>
17      <h1>{data.title}</h1>
18      <p>{data.shortDescription}</p>
19    </section>
20  )
21}
note
It's important that the parent query or fragment selects any keyFields for objects passed to the from option. Without this, we'd have no way to identify the object with the cache and the data returned from useFragment would be incomplete. If you forget to include key fields in the parent object, you will see a warning in the console.

Nesting fragments in other fragments

note
You can nest fragments with or without data masking (for an example, see the section on colocating fragments.) The following section describes how you can use masking in components with useFragment.

As your UI grows in complexity, it is common to split up components into smaller, more reusable chunks. As a result you may end up with more deeply nested components that have their own data requirements. Much like queries, we can nest fragments within other fragments.

Let's add a Comment component that will be used by PostDetails to render the topComment for the post.

JavaScript
Comment.jsx
1export const COMMENT_FRAGMENT = gql`
2  fragment CommentFragment on Comment {
3    postedBy {
4      displayName
5    }
6    createdAt
7    content
8  }
9`
10
11export default function Comment({ comment }) {
12  const { data, complete } = useFragment({
13    fragment: COMMENT_FRAGMENT,
14    from: comment,
15  });
16
17  // ... render comment details
18}

Much like PostDetails, we used useFragment to read the CommentFragment fragment data since it is masked and not available on the comment prop.

We can now use the Comment component and CommentFragment fragment in the PostDetails component to render the topComment.

JavaScript
PostDetails.jsx
1import { COMMENT_FRAGMENT } from "./Comment";
2
3export const POST_DETAILS_FRAGMENT = gql`
4  fragment PostDetailsFragment on Post {
5    title
6    shortDescription
7    topComment {
8      id
9      ...CommentFragment
10    }
11  }
12
13  ${COMMENT_FRAGMENT}
14`;
15
16export default function PostDetails({ post }) {
17  const { data, complete } = useFragment({
18    fragment: POST_DETAILS_FRAGMENT,
19    from: post,
20    fragmentName: "PostDetailsFragment",
21  });
22
23  // complete check omitted for brevity
24
25  return (
26    <section>
27      <h1>{data.title}</h1>
28      <p>{data.shortDescription}</p>
29      <Comment comment={data.topComment} />
30    </section>
31  );
32}
note
We added the fragmentName option to useFragment in PostDetails. This is needed because we've added another fragment definition (CommentFragment) to the POST_DETAILS_FRAGMENT document. fragmentName tells useFragment that it should use the PostDetailsFragment fragment definition when querying for data in the cache.

If we inspect the data property returned by useFragment in PostDetails, we'll see that only the fields included by the PostDetailsFragment fragment are a part of the object.

JSON
1{
2  "__typename": "Post",
3  "title": "The Amazing Adventures of Data Masking",
4  "shortDescription": "In this article we dive into...",
5  "topComment": {
6    "__typename": "Comment",
7    "id": "1"
8  }
9}

Throughout this example, You'll notice that we never touched the GetPosts query as a result of this change. Because we included CommentFragment with PostDetailsFragment, it was added to the query automatically. Colocating fragments like this is a powerful pattern that, when combined with data masking, provide very self-isolated components.

tip
We recommend that parent components only add fragments defined by their directly-rendered children to prevent coupling with more deeply nested components. In this example, the GetPosts query did not include the CommentFragment fragment directly but rather it relied on the PostDetails component to include it with the PostDetailsFragment fragment.

Working with other operation types

Data masking is not limited to queries but also extends to other operation types. As a rule of thumb, any value that is used to read data from a request-based API is masked. APIs that perform cache updates are never masked.

Refer to the code samples below to see what data is masked in mutations and subscriptions.

Mutations

For more information about mutations, visit the mutations page.

JavaScript
1// data is masked
2const [mutate, { data }] = useMutation(MUTATION, {
3  onCompleted: (data) => {
4    // data is masked
5  },
6  update: (cache, { data }) => {
7    // data is unmasked
8  },
9  refetchQueries: ({ data }) => {
10    // data is unmasked
11  },
12  updateQueries: {
13    ExampleQuery: (previous, { mutationResult }) => {
14      // mutationResult is unmasked
15    }
16  }
17});
18
19async function runMutation() {
20  const { data } = await mutate()
21
22  // data is masked
23}
JavaScript
1// data is masked
2const { data } = await client.mutate({
3  update: (cache, { data }) => {
4    // data is unmasked
5  },
6  refetchQueries: ({ data }) => {
7    // data is unmasked
8  },
9  updateQueries: {
10    ExampleQuery: (previous, { mutationResult }) => {
11      // mutationResult is unmasked
12    }
13  }
14});

Subscriptions

For more information about subscriptions, visit the subscriptions page.

JavaScript
1function MyComponent() {
2  // data is masked
3  const { data } = useSubscription(SUBSCRIPTION, {
4    onData: ({ data }) => {
5      // data is unmasked
6    }
7  });
8}
JavaScript
1const observable = client.subscribe({ query: SUBSCRIPTION });
2
3observable.subscribe({
4  next: ({ data }) => {
5    // data is masked
6  },
7});
JavaScript
1const { subscribeToMore } = useQuery(QUERY);
2
3function startSubscription() {
4  subscribeToMore({
5    document: SUBSCRIPTION,
6    updateQuery: (queryData, { subscriptionData }) => {
7      // queryData is unmasked
8      // subscriptionData is unmasked
9    }
10  })
11}

Selectively unmasking fragment data

As you work with data masking more extensively, you may need access to the full operation result. Apollo Client includes an @unmask directive you can apply to fragment spreads. Adding @unmask to a fragment spread makes the fragment data available.

note
The @unmask directive is an escape hatch. First try adding additional needed fields to the operation before relying on @unmask. As an exception, you might use @unmask frequently to adopt data masking in an existing application.
GraphQL
1query GetPosts {
2  posts {
3    id
4    ...PostFragment @unmask
5  }
6}

Only fragments marked with @unmask will unmask the results. Fragments not marked with @unmask will remain masked.

GraphQL
1query GetPosts {
2  posts {
3    id
4    ...PostFragment @unmask
5    # This data remains masked
6    ...PostDetailsFragment
7  }
8}

Using with TypeScript

Apollo Client provides robust TypeScript support for data masking. We've integrated data masking with GraphQL Codegen and the type format generated by its Fragment Masking feature.

Masked types don't include fields from fragment spreads. As an example, let's use the following query.

GraphQL
1query GetCurrentUser {
2  currentUser {
3    id
4    ...ProfileFragment
5  }
6}
7
8fragment ProfileFragment on User {
9  name
10  age
11}

The type definition for the query might resemble the following:

TypeScript
1type GetCurrentUserQuery = {
2  currentUser: {
3    __typename: "User";
4    id: string;
5    name: string;
6    age: number;
7  } | null
8}
note
This example does not use GraphQL Codegen's true type output since it includes additional types that map scalar values differently.

This version of the GetCurrentUserQuery type is unmasked since it includes fields from the ProfileFragment.

On the other hand, masked types don't include fields defined in fragments.

TypeScript
1type GetCurrentUserQuery = {
2  currentUser: {
3    __typename: "User";
4    id: string;
5    // omitted: additional internal metadata
6  } | null
7}

Generating masked types

You generate masked types with either the typescript-operations plugin or the client preset. You can generate masked types at any stage in the adoption process, even before enabling dataMasking in your client instance. Until you opt in to use masked types, the client unwraps them and provides the full operation type.

The following sections show how to configure GraphQL Codegen to output masked types.

With the typescript-operations plugin
note
Support for the @unmask directive was introduced with @graphql-codegen/typescript-operations v4.4.0

Add the following configuration to your GraphQL Codegen config.

TypeScript
codegen.ts
1const config: CodegenConfig = {
2  // ...
3  generates: {
4    "path/to/types.ts": {
5      plugins: ["typescript-operations"],
6      config: {
7        // ...
8        inlineFragmentTypes: "mask",
9        customDirectives: {
10          apolloUnmask: true
11        }
12      }
13    }
14  }
15}
With the client-preset
note
Support for the @unmask directive was introduced with @graphql-codegen/client-preset v4.5.1

Add the following configuration to your GraphQL Codegen config.

caution
The Fragment Masking feature in the client preset is incompatible with Apollo Client's data masking feature. You need to turn off Fragment Masking in your configuration (included below). If you use the generated useFragment function, you must use Apollo Client's useFragment hook instead.
TypeScript
codegen.ts
1const config: CodegenConfig = {
2  // ...
3  generates: {
4    "path/to/gql/": {
5      preset: "client",
6      presetConfig: {
7        // ...
8        // disables the incompatible GraphQL Codegen fragment masking feature
9        fragmentMasking: false,
10        inlineFragmentTypes: "mask",
11        customDirectives: {
12          apolloUnmask: true
13        }
14      }
15    }
16  }
17}

Opting in to use masked types

note
We recommend that you opt in to use masked types only after you've enabled dataMasking in your ApolloClient instance.

By default, the client unwraps operation types and provides the full operation result type. To prevent this behavior and have the client use masked types, you need to opt in. This strategy allows for incremental adoption in your application and avoids the need to update large parts of your application to satisfy the change in types.

You can opt in to use the masked types in one of two ways.

Opting in globally

To turn on masked types for your whole application at once, modify the DataMasking exported type from @apollo/client using TypeScript's declaration merging ability.

tip
We recommend this approach for most cases. Use this approach with the @unmask directive for the best path to incremental adoption.

First, create a TypeScript file that will be used to modify the DataMasking type.

TypeScript
apollo-client.d.ts
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import '@apollo/client';
4
5declare module "@apollo/client" {
6  interface DataMasking {
7    enabled: true;
8  }
9}
note
This example uses apollo-client.d.ts as the file name to make it easily identifiable. You can name this file as you wish.

With enabled set to true, any request-based API will type data using the masked type and prevent the client from unwrapping it.

Opting in per operation

If you prefer an incremental approach, you can opt in to use masked types per operation. This can be useful when your application creates multiple Apollo Client instances where only a subset enables data masking.

Apollo Client provides a Masked helper type that tells the client to use the masked type directly. You can use this with TypedDocumentNode or with generic arguments.

TypeScript
1import { Masked, TypedDocumentNode } from "@apollo/client";
2
3// With TypedDocumentNode
4const QUERY: TypedDocumentNode<Masked<QueryType>, VarsType> = gql`
5  # ...
6`;
7
8// with generic arguments
9const { data } = useQuery<Masked<QueryType>, VarsType>(QUERY)

The use of TypedDocumentNode with the Masked type is common enough that Apollo Client provides a MaskedDocumentNode convenience type as a replacement for TypedDocumentNode. It is simply a shortcut for TypedDocumentNode<Masked<QueryType>, VarsType>.

TypeScript
1import { MaskedDocumentNode } from "@apollo/client";
2
3const QUERY: MaskedDocumentNode<QueryType, VarsType> = gql`
4  # ...
5`;

Using with fragments

When using colocated fragments with your components, it's best to ensure the object passed to your component is done in a type-safe way. This means:

  • TypeScript prevents you from accessing fields on the object that may be defined with the parent.

  • The object passed to the component is guaranteed to contain a fragment reference of the same type.

Apollo Client provides the FragmentType helper type for this purpose. As an example, let's use the PostDetails fragment from previous sections.

TypeScript
PostDetails.tsx
1import type { FragmentType } from "@apollo/client";
2import type { PostDetailsFragment } from "./path/to/gql/types.ts";
3
4export const POST_DETAILS_FRAGMENT: TypedDocumentNode<
5  PostDetailsFragment
6> = gql`
7  fragment PostDetailsFragment on Post {
8    title
9    shortDescription
10  }
11`;
12
13interface PostDetailsProps {
14  post: FragmentType<PostDetailsFragment>
15}
16
17function PostDetails({ post }: PostDetailsProps) {
18  const { data } = useFragment({
19    fragment: POST_DETAILS_FRAGMENT,
20    from: post,
21  });
22
23  // ...
24}

Using properties from the post prop instead of the data from useFragment results in a TypeScript error similar to the following:

TypeScript
1function PostDetails({ post }: PostDetailsProps) {
2  // ...
3
4  post.title
5  // ❌ Property 'title' does not exist on type '{ " $fragmentRefs"?: { PostDetailsFragment: PostDetailsFragment; } | undefined; }'
6}

FragmentType also prevents parent components from accidentally omitting fragment spreads for child components, regardless of whether the field selection satisfies the fragment's data requirements.

TypeScript
Posts.tsx
1const GET_POSTS = gql`
2  query GetPosts {
3    posts {
4      id
5      title
6      shortDescription
7    }
8  }
9`;
10
11export default function Posts() {
12  // ...
13
14  return (
15    <div>
16      {allPosts.map((post) => (
17        <PostDetails key={post.id} post={post} />
18        // ❌ Type '{ __typename: "Post"; id: string; title: string; shortDescription: string; }' has no properties in common with type '{ " $fragmentRefs"?: { PostDetailsFragment: PostDetailsFragment; } | undefined; }'.
19      ))}
20    </div>
21  );
22}

In this example, the GetPosts query selects enough fields to satisfy the PostDetails data requirements, but TypeScript warns us because the PostDetailsFragment was not included in the GetPosts query.

Unwrapping masked types

On rare occasions, you may need access to the unmasked type of a particular operation. Apollo Client provides the Unmasked helper type that unwraps masked types and removes meta information on the type.

note
This is the same helper type the client uses when unwrapping types while data masking is turned off or for APIs that use the full result.
TypeScript
1import { Unmasked } from "@apollo/client";
2
3type QueryType = {
4  currentUser: {
5    __typename: "User";
6    id: string;
7    name: string;
8  } & { " $fragmentRefs"?: { UserFragment: UserFragment } }
9}
10
11type UserFragment = {
12  __typename: "User";
13  age: number | null;
14} & { " $fragmentName"?: "UserFragment" }
15
16type UnmaskedQueryType = Unmasked<QueryType>;
17//   ^? type UnmaskedQueryType = {
18//        currentUser: {
19//          __typename: "User";
20//          id: string;
21//          name: string;
22//          age: number | null;
23//        }
24//      }
note
This example does not use GraphQL Codegen's true type output since it includes additional types that map scalar values differently.

Incremental adoption in an existing application

Existing applications can take advantage of the data masking features through an incremental adoption approach. This section will walk through the steps needed to adopt data masking in a larger codebase.

1. Apply the @unmask directive to all fragment spreads

Before enabling the dataMasking flag in the client, it is wise to ensure that your components continue to receive full results to avoid breakages. We can use the @unmask directive to handle this.

tip
We recommend using @unmask in migrate mode to enable development-only warnings when accessing would-be masked fields. Learn more about migrate mode in the @unmask docs.
GraphQL
1query GetPost($id) {
2  post(id: $id) {
3    id
4    ...PostDetails @unmask(mode: "migrate")
5  }
6}

This is rather tedious to do by hand for large applications. Apollo Client provides a codemod that applies @unmask to your GraphQL documents for you. To use the codemod:

  1. Clone the apollo-client repository

    sh
    1git clone https://github.com/apollographql/apollo-client.git
  2. Install dependencies in apollo-client

    sh
    1npm install
  3. Run the codemod via jscodeshift against your codebase.

    sh
    1npx jscodeshift -t ../path/to/apollo-client/scripts/codemods/data-masking/unmask.ts --extensions ts --parser ts ./src/**/*.ts --mode migrate
    note
    This command uses the --mode migrate option to enable migrate mode on all @unmask directives. Omit this option if you prefer to use @unmask without the development-only warnings.

The codemod supports .js, .jsx, .ts, .tsx, .graphql, and .gql files. Use the graphql parser when running the codemod against GraphQL files.

By default, the codemod searches for gql and graphql tags in source files. If your application uses a custom name, use the --tag option to specify the name. Use --tag more than once to specify multiple names.

sh
1npx jscodeshift ... --tag myGql
caution
Comments in GraphQL documents may be lost after running the codemod because the graphql library's print function used by the codemod doesn't retain comments. Always check the output from changes made by the codemod before comitting them.

2. Enable dataMasking

With fragments unmasked, it is safe to enable data masking in your application. Add the dataMasking option to your client instance to enable it.

JavaScript
1new ApolloClient({
2  dataMasking: true,
3  // ...
4});

Enabling data masking early in the adoption process makes it much easier to adopt for newly added queries and fragments since masking becomes the default behavior. Ideally data masking is enabled in the same pull request as the @unmask changes to ensure that no new queries and fragments are introduced to the codebase without the @unmask modifications applied.

3. Generate and opt in to use masked types

If you are using TypeScript in your application, you will need to update your GraphQL Codegen configuration to generate masked types. Once you generate masked types, opt in to use the masking types with your client.

Learn more about using TypeScript with data masking in the "Using with TypeScript" section.

4. Use useFragment

With data masking enabled, you can now begin the process of refactoring your components to use data masking. It is easiest to look for areas of the codebase where you see field access warnings in the console on would-be masked fields (requires migrate mode).

Refactor components that consume query data from props to use useFragment instead. Use the data property returned from useFragment to get the field value instead of the prop.

JavaScript
1function PostDetails({ post }) {
2  const { data, complete } = useFragment({
3    fragment: POST_DETAILS_FRAGMENT,
4    from: post,
5  })
6
7  // ... use `data` instead of `post`
8}

As you make these changes, you will begin to see warnings disappear. Repeat this process until you no longer see warnings in the console.

When you no longer see field access warnings, it is safe to remove the @unmask directive from your query.

diff
1query GetPosts {
2  posts {
3    id
4-   ...PostDetails @unmask(mode: "migrate")
5+   ...PostDetails
6  }
7}

Repeat this process until all @unmask directives have been removed from your codebase.

Congratulations 🎉! Your application is now using data masking everywhere 😎.

Feedback

Edit on GitHub

Forums