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
export
s the fragment from theComment.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:
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 createFragmentRegistry
Since 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});
useFragment
Since 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 id
s 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: 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: 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.
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.
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
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.
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. 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
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
Add the following configuration to your GraphQL Codegen config.
useFragment
function, you must use Apollo Client's useFragment
hook instead.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 customDirectives: {
11 apolloUnmask: true
12 }
13 },
14 config: {
15 inlineFragmentTypes: "mask",
16 }
17 }
18 }
19}
Opting in to use masked types
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.
@unmask
directive for the best path to incremental adoption.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 enabled: true;
8 }
9}
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.
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.
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-client
repositorysh1git clone https://github.com/apollographql/apollo-client.git
Install dependencies in
apollo-client
sh1npm install
Run the codemod via
jscodeshift
against your codebase.sh1npx jscodeshift -t ../path/to/apollo-client/scripts/codemods/data-masking/unmask.ts --extensions ts --parser ts ./src/**/*.ts --mode migrate
noteThis 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.
1npx jscodeshift ... --tag myGql
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.
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.
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 😎.