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:
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:
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:
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:
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 theComment.jscomponent 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:
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
importCOMMENT_FRAGMENTbecause it's declared in another file.We add our fragment definition to the
GET_POST_DETAILSgqltemplate literal via a placeholder (${COMMENT_FRAGMENT})We include the
CommentFragmentfragment in our query with standard...notation.
Registering named fragments using createFragmentRegistryRequires ≥ 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.
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.
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.
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:
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.
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:
1export const { fragmentRegistry } = createFragmentRegistry();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.
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.
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:
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:
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`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:
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:
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:
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:
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:
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:
1import possibleTypes from './path/to/possibleTypes.json';
2
3const cache = new InMemoryCache({
4 possibleTypes,
5});useFragmentRequires ≥ 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.
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:
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.
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}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.
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}1function Item(props) {
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}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.
1function Item(props: { item: { __typename: 'Item', id: number }}) {
2 const { complete, data } = useFragment({
3 fragment: ItemFragment,
4 fragmentName: "ItemFragment",
5 from: props.item
6 });
7
8 return <li>{complete ? data.text : "incomplete"}</li>;
9}1function Item(props) {
2 const { complete, data } = useFragment({
3 fragment: ITEM_FRAGMENT,
4 fragmentName: "ItemFragment",
5 from: props.item
6 });
7
8 return <li>{complete ? data.text : "incomplete"}</li>;
9}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.
useSuspenseFragmentRequires ≥ 3.13.0
For those that have integrated with React Suspense, useSuspenseFragment is available as a drop-in replacement for useFragment. useSuspenseFragment works identically to useFragment but will suspend while data is incomplete.
Let's update the example from the previous section to use useSuspenseFragment. First, we'll update our Item component and replace useFragment with useSuspenseFragment. Since we are using Suspense, we no longer have to check for a complete property to determine if the result is complete because the component will suspend otherwise.
1import { useSuspenseFragment } from "@apollo/client";
2
3function Item(props) {
4 const { data } = useSuspenseFragment({
5 fragment: ITEM_FRAGMENT,
6 fragmentName: "ItemFragment",
7 from: props.item
8 });
9
10 return <li>{data.text}</li>;
11}Next, we'll will wrap our Item components in a Suspense boundary to show a loading indicator if the data from ItemFragment is not complete. Since we're using Suspense, we'll replace useQuery with useSuspenseQuery as well:
1function List() {
2 const { data } = useSuspenseQuery(listQuery);
3
4 return (
5 <ol>
6 {data.list.map(item => (
7 <Suspense fallback={<Spinner />}>
8 <Item key={item.id} item={item}/>
9 </Suspense>
10 ))}
11 </ol>
12 );
13}And that's it! Suspense made our Item component a bit more succinct since we no longer need to check the complete property to determine if we can safely use data.
useSuspenseFragment will not suspend when rendered as a child of a query component. In this example useSuspenseQuery loads the full query data before each Item is rendered so the data inside each fragment is already complete. The Suspense boundary in this example ensures that a loading spinner is shown if field data is removed for any given item in the list in the cache, such as when a manual cache update is performed.Using useSuspenseFragment with @defer
useSuspenseFragment is helpful when combined with the @defer directive to show a loading state while the fragment data is streamed to the query. Let's update our GetItemList query to defer loading the ItemFragment's fields.
1query GetItemList {
2 list {
3 id
4 ...ItemFragment @defer
5 }
6}Our list will now render as soon as our list returns but before the data for ItemFragment is loaded.
from option are not deferred. If they are, you risk suspending the useSuspenseFragment hook forever. If you need to defer loading key fields, conditionally render the component until the object passed to the from option is identifiable by the cache.Data maskingRequires ≥ 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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}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
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.
useFragment.Let's add a Comment component that will be used by PostDetails to render the topComment for the post.
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.
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}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.
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.
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.
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}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.
1function MyComponent() {
2 // data is masked
3 const { data } = useSubscription(SUBSCRIPTION, {
4 onData: ({ data }) => {
5 // data is unmasked
6 }
7 });
8}1const observable = client.subscribe({ query: SUBSCRIPTION });
2
3observable.subscribe({
4 next: ({ data }) => {
5 // data is masked
6 },
7});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.
@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.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.
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.
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:
1type GetCurrentUserQuery = {
2 currentUser: {
3 __typename: "User";
4 id: string;
5 name: string;
6 age: number;
7 } | null
8}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.
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. The following sections show how to configure GraphQL Codegen to output masked types.
With the typescript-operations plugin
Add the following configuration to your GraphQL Codegen config.
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
You can't use the client-preset Fragment Masking and Apollo Client's data masking features simultaneously.
The incompatibility between the features is in runtime behavior only.
Apollo's data masking uses the same type output generated by CodeGen's Fragment Masking feature.
To migrate from CodeGen's fragment masking feature to Apollo Client's data masking, follow these steps:
Replace the generated
useFragmentfunction, with Apollo Client'suseFragmenthook.Turn off Fragment Masking in your GraphQL Codegen config, along with these additions:
TypeScriptcodegen.ts1const 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 }, 11 config: { 12 customDirectives: { 13 apolloUnmask: true 14 } 15 inlineFragmentTypes: "mask", 16 } 17 } 18 } 19}Enable data masking in Apollo Client.
Setting a types mode for masked typesRequires ≥ 3.12.5
Apollo Client provides different modes to work with operation types throughout your application. You change the mode by modifying the DataMasking exported type from the @apollo/client package using TypeScript's declaration merging ability. Specifying a mode is optional and only needed when you prefer to use a different mode other than the default.
To modify the data masking mode used in the client, first create a TypeScript file that will be used to modify the DataMasking type.
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 mode: "preserveTypes";
8 }
9}apollo-client.d.ts as the file name to make it easily identifiable. You can name this file as you wish.Modes
preserveTypes (default)
The default preserveTypes mode makes no modification to the operation types regardless of whether the type definitions are masked or unmasked. This provides a simpler upgrade path when you're ready to incrementally adopt data masking.
unmask
Setting the mode to unmask will unwrap masked types and provide the full result type. Use this mode when you generate masked types but need to maintain access to the full result type, such as using this with per-operation masked types.
Using per-operation masked types
unmask. Using this with preserveTypes has no effect.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.
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>.
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.
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:
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.
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.
mode is set to unmask or for APIs that use the full result.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// }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.
@unmask in migrate mode to enable development-only warnings when accessing would-be masked fields. Learn more about migrate mode in the @unmask docs.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:
Clone the
apollo-clientrepositorysh1git clone https://github.com/apollographql/apollo-client.gitInstall dependencies in
apollo-clientsh1npm installRun the codemod via
jscodeshiftagainst your codebase.sh1npx jscodeshift -t ../path/to/apollo-client/scripts/codemods/data-masking/unmask.ts --extensions ts --parser ts ./src/**/*.ts --mode migratenoteThis command uses the--mode migrateoption to enable migrate mode on all@unmaskdirectives. Omit this option if you prefer to use@unmaskwithout 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.
1npx jscodeshift ... --tag myGqlgraphql 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.
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
@unmaskchanges to ensure that no new queries and fragments are introduced to the codebase without the@unmaskmodifications applied.
3. Generate masked types
If you are using TypeScript in your application, you will need to update your GraphQL Codegen configuration to generate masked types.
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.
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.
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 😎.