docs/source/data/fragments.mdx
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:
fragment NameParts on Person {
firstName
lastName
}
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:
query GetPerson {
people(id: "7") {
...NameParts
avatar(size: LARGE)
}
}
Based on our NameParts definition, the above query is equivalent to:
query GetPerson {
people(id: "7") {
firstName
lastName
avatar(size: LARGE)
}
}
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.
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:
import { gql } from "@apollo/client";
export const COMMENT_FRAGMENT = gql`
fragment CommentFragment on Comment {
id
postedBy {
username
displayName
}
createdAt
content
}
`;
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:
import { gql } from "@apollo/client";
import { COMMENT_FRAGMENT } from "./Comment";
const GET_POST_DETAILS = gql`
query GetPostDetails($postId: ID!) {
post(postId: $postId) {
title
body
author
comments {
...CommentFragment
}
}
}
${COMMENT_FRAGMENT}
`;
// ...PostDetails component definition...
import COMMENT_FRAGMENT because it's declared in another file.GET_POST_DETAILS gql template literal via a placeholder (${COMMENT_FRAGMENT})CommentFragment fragment in our query with standard ... notation.createFragmentRegistryRegistering 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.
Let's look at an example in React.
import { ApolloClient, gql, InMemoryCache } from "@apollo/client";
import { createFragmentRegistry } from "@apollo/client/cache";
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache: new InMemoryCache({
fragments: createFragmentRegistry(gql`
fragment ItemFragment on Item {
id
text
}
`),
}),
});
Since ItemFragment was registered with InMemoryCache, it can be referenced by name, as seen below, with the fragment spread inside of the GetItemList query.
const listQuery = gql`
query GetItemList {
list {
...ItemFragment
}
}
`;
function ToDoList() {
const { data } = useQuery(listQuery);
return (
<ol>
{data?.list.map((item) => (
<Item key={item.id} text={item.text} />
))}
</ol>
);
}
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:
import { ApolloClient, gql, InMemoryCache } from "@apollo/client";
import { createFragmentRegistry } from "@apollo/client/cache";
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache: new InMemoryCache({
fragments: createFragmentRegistry(gql`
fragment ItemFragment on Item {
id
text
...ExtraFields
}
fragment ExtraFields on Item {
isCompleted
}
`),
}),
});
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.
const GET_ITEM_LIST = gql`
query GetItemList {
list {
...ItemFragment
}
}
fragment ExtraFields on Item {
createdBy
}
`;
function ToDoList() {
const { data } = useQuery(GET_ITEM_LIST);
return (
<ol>
{data?.list.map((item) => (
<Item key={item.id} text={item.text} author={item.createdBy} />
))}
</ol>
);
}
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:
export const { fragmentRegistry } = createFragmentRegistry();
import { fragmentRegistry } from "./fragmentRegistry";
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache: new InMemoryCache({
fragments: fragmentRegistry,
}),
});
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.
import { gql } from "@apollo/client";
import { fragmentRegistry } from "./fragmentRegistry";
// Define the fragment outside the component to ensure it gets registered when this module is loaded.
const ITEM_FRAGMENT = gql`
fragment ItemFragment on Item {
# ...
}
`;
fragmentRegistry.register(ITEM_FRAGMENT);
function TodoItem() {
// ...
}
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 registry until after the query begins executing. Move the fragment definition to a shared file that isn't lazy-loaded. </Caution>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:
graph TD
subgraph FeedPageGroup[" "]
direction TB
FeedPage[<FeedPage />]
FeedQuery["query FeedQuery { feed { ...FeedEntryFragment } }"]
FeedPage ~~~ FeedQuery
end
subgraph FeedEntryGroup[" "]
direction TB
FeedEntry["<FeedEntry />"]
FeedEntryFragment["fragment FeedEntry { ...EntryInfoFragment ...VoteButtonsFragment }"]
FeedEntry ~~~ FeedEntryFragment
end
subgraph EntryInfoGroup[" "]
direction TB
EntryInfo["<EntryInfo />"]
EntryInfoFragment["fragment EntryInfo { ... }"]
EntryInfo ~~~ EntryInfoFragment
end
subgraph VoteButtonsGroup[" "]
direction TB
VoteButtons["<VoteButtons />"]
VoteButtonsFragment["fragment VoteButtons { ... }"]
VoteButtons ~~~ VoteButtonsFragment
end
FeedPageGroup --> FeedEntryGroup
FeedEntryGroup --> EntryInfoGroup
FeedEntryGroup --> VoteButtonsGroup
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.
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:
export const VOTE_BUTTONS_FRAGMENT = gql`
fragment VoteButtonsFragment on FeedEntry {
score
vote {
choice
}
}
`;
After you define a fragment in a child component, the parent component can refer to it in its own colocated fragments, like so:
export const FEED_ENTRY_FRAGMENT = gql`
fragment FeedEntryFragment on FeedEntry {
commentCount
repository {
full_name
html_url
owner {
avatar_url
}
}
...VoteButtonsFragment
...EntryInfoFragment
}
${VOTE_BUTTONS_FRAGMENT}
${ENTRY_INFO_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.
When loading .graphql files with graphql-tag/loader, include fragments using import statements. For example:
#import "./someFragment.graphql"
This makes the contents of someFragment.graphql available to the current file. See the Webpack Fragments section for additional details.
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:
query AllCharacters {
allCharacters {
name
... on Jedi {
side
}
... on Droid {
model
}
}
}
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.
possibleTypes manuallyUse 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:
const cache = new InMemoryCache({
possibleTypes: {
Character: ["Jedi", "Droid"],
Test: ["PassingTest", "FailingTest", "SkippedTest"],
Snake: ["Viper", "Python"],
},
});
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.
possibleTypes automaticallyThe following script translates a GraphQL introspection query into a possibleTypes configuration object:
const fetch = require("cross-fetch");
const fs = require("fs");
fetch(`${YOUR_API_HOST}/graphql`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
variables: {},
query: `
{
__schema {
types {
kind
name
possibleTypes {
name
}
}
}
}
`,
}),
})
.then((result) => result.json())
.then((result) => {
const possibleTypes = {};
result.data.__schema.types.forEach((supertype) => {
if (supertype.possibleTypes) {
possibleTypes[supertype.name] = supertype.possibleTypes.map(
(subtype) => subtype.name
);
}
});
fs.writeFile(
"./possibleTypes.json",
JSON.stringify(possibleTypes),
(err) => {
if (err) {
console.error("Error writing possibleTypes.json", err);
} else {
console.log("Fragment types successfully extracted!");
}
}
);
});
You can then import the generated possibleTypes JSON module into the file where you create your InMemoryCache:
import possibleTypes from "./path/to/possibleTypes.json";
const cache = new InMemoryCache({
possibleTypes,
});
possibleTypes with GraphQL CodegenGraphQL 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:
import possibleTypes from "./path/to/possibleTypes.json";
const cache = new InMemoryCache({
possibleTypes,
});
useFragmentThe 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.
Given the following fragment definition:
const ITEM_FRAGMENT = gql`
fragment ItemFragment on Item {
text
}
`;
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.
const listQuery = gql`
query GetItemList {
list {
id
...ItemFragment
}
}
${ITEM_FRAGMENT}
`;
function List() {
const { loading, data } = useQuery(listQuery);
return (
<ol>
{data?.list.map((item) => (
<Item key={item.id} item={item} />
))}
</ol>
);
}
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.
function Item(props: { id: number }) {
const { complete, data } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: {
__typename: "Item",
id: props.id,
},
});
return <li>{complete ? data.text : "incomplete"}</li>;
}
function Item(props) {
const { complete, data } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: {
__typename: "Item",
id: props.id,
},
});
return <li>{complete ? data.text : "incomplete"}</li>;
}
You may instead prefer to pass the whole item as a prop to the Item component. This makes the from option more concise.
function Item(props: { item: { __typename: "Item"; id: number } }) {
const { complete, data } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: props.item,
});
return <li>{complete ? data.text : "incomplete"}</li>;
}
function Item(props) {
const { complete, data } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: props.item,
});
return <li>{complete ? data.text : "incomplete"}</li>;
}
See the API reference for more details on the supported options.
<MinVersion version="4.1">Sometimes your component might use a fragment to select fields for an array of items that are received from props. You can use the useFragment hook to watch for changes on each array item by providing the array to the from option.
When you provide an array to the from option, the data property returned from useFragment is an array where each item corresponds to an item with the same index in the from option. If all of the items returned in data are complete, the complete property is set to true and the dataState property is set to "complete". If at least one item in the array is incomplete, the complete property is set to false and the dataState property is set to "partial".
function Items(props: { items: Array<{ __typename: "Item"; id: number }> }) {
const { data, complete } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: props.items,
});
if (!complete) {
return null;
}
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
function Items(props) {
const { data, complete } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: props.items,
});
if (!complete) {
return null;
}
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
If the array provided to the from option is an empty array, the returned data is an empty array with the complete property set to true and dataState property set to "complete".
null valuesDepending on the GraphQL schema, it's possible the array might contain null values. When useFragment is provided an array that contains null values to the from property, useFragment returns those items as null in the data property and treats these items as complete. This means if all non-null items in the array are also complete, the whole result is complete.
const { data, dataState, complete } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: [{ __typename: "Item", id: 1 }, { __typename: "Item", id: 2 }, null],
});
console.log({ data, dataState, complete });
// {
// data: [
// { __typename: "Item", id: 1, text: "..." },
// { __typename: "Item", id: 2, text: "..." },
// null
// ],
// dataState: "complete",
// complete: true
// }
If the from array contains null values for every item, the result returned from useFragment contains all null values, the complete property is set to true, and the dataState property is set to "complete".
const { data, dataState, complete } = useFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: [null, null, null],
});
console.log({ data, dataState, complete });
// {
// data: [null, null, null],
// dataState: "complete",
// complete: true
// }
useSuspenseFragmentFor 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.
import { useSuspenseFragment } from "@apollo/client";
function Item(props) {
const { data } = useSuspenseFragment({
fragment: ITEM_FRAGMENT,
fragmentName: "ItemFragment",
from: props.item,
});
return <li>{data.text}</li>;
}
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:
function List() {
const { data } = useSuspenseQuery(listQuery);
return (
<ol>
{data.list.map((item) => (
<Suspense fallback={<Spinner />}>
<Item key={item.id} item={item} />
</Suspense>
))}
</ol>
);
}
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 with @deferuseSuspenseFragment 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.
query GetItemList {
list {
id
...ItemFragment @defer
}
}
Our list will now render as soon as our list returns but before the data for ItemFragment is loaded.
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.
import { POST_DETAILS_FRAGMENT } from "./PostDetails";
const GET_POSTS = gql`
query GetPosts {
posts {
id
...PostDetailsFragment
}
}
${POST_DETAILS_FRAGMENT}
`;
export default function Posts({ includeUnpublishedPosts }) {
const { data, loading } = useQuery(GET_POSTS);
const posts = data?.posts ?? [];
if (loading) {
return <Spinner />;
}
const allPosts =
includeUnpublishedPosts ? posts : posts.filter((post) => post.publishedAt);
if (allPosts.length === 0) {
return <div>No posts to display</div>;
}
return (
<div>
{allPosts.map((post) => (
<PostDetails key={post.id} post={post} />
))}
</div>
);
}
The following PostDetails.jsx defines the fragment for post details and the associated UI elements.
export const POST_DETAILS_FRAGMENT = gql`
fragment PostDetailsFragment on Post {
title
shortDescription
publishedAt
}
`;
export default function PostDetails({ post }) {
return (
<section>
<h1>{post.title}</h1>
<p>{post.shortDescription}</p>
<p>
{post.publishedAt ?
`Published: ${formatDate(post.publishedAt)}`
: "Private"}
</p>
</section>
);
}
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.
export const POST_DETAILS_FRAGMENT = gql`
fragment PostDetailsFragment on Post {
title
shortDescription
}
`;
export default function PostDetails({ post }) {
return (
<section>
<h1>{post.title}</h1>
<p>{post.shortDescription}</p>
</section>
);
}
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.
To enable data masking in Apollo Client, set the dataMasking flag in the ApolloClient constructor to true.
const client = new ApolloClient({
dataMasking: true,
// ...
});
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.
Let's revisit the example from the previous section.
const GET_POSTS = gql`
query GetPosts {
posts {
id
...PostDetailsFragment
}
}
${POST_DETAILS_FRAGMENT}
`;
export default function Posts({ includeUnpublishedPosts }) {
const { data, loading } = useQuery(GET_POSTS);
// ...
}
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.
{
"posts": [
{
"__typename": "Post",
"id": "1"
},
{
"__typename": "Post",
"id": "2"
}
]
}
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.
const GET_POSTS = gql`
query GetPosts {
posts {
id
publishedAt
...PostDetailsFragment
}
}
${POST_DETAILS_FRAGMENT}
`;
Now if we inspect data, we'll see that publishedAt is available to the Posts component.
{
"posts": [
{
"__typename": "Post",
"publishedAt": "2024-01-01",
"id": "1"
},
{
"__typename": "Post",
"publishedAt": null,
"id": "2"
}
]
}
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.
function PostDetails({ post }) {
const { data, complete } = useFragment({
fragment: POST_DETAILS_FRAGMENT,
from: post,
});
// ...
}
Now we use the data property returned from useFragment to render the details from the post.
function PostDetails({ post }) {
const { data, complete } = useFragment({
fragment: POST_DETAILS_FRAGMENT,
from: post,
});
// It's a good idea to check the `complete` flag to ensure all data was
// successfully queried from the cache. This can indicate a potential
// issue with the cache configuration or parent object when `complete`
// is `false`.
if (!complete) {
return null;
}
return (
<section>
<h1>{data.title}</h1>
<p>{data.shortDescription}</p>
</section>
);
}
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.
<Note> You can nest fragments with or without data masking (for an example, see the section on [colocating fragments](#colocating-fragments).) This section describes how you can use masking in components with `useFragment`. </Note>Let's add a Comment component that will be used by PostDetails to render the topComment for the post.
export const COMMENT_FRAGMENT = gql`
fragment CommentFragment on Comment {
postedBy {
displayName
}
createdAt
content
}
`;
export default function Comment({ comment }) {
const { data, complete } = useFragment({
fragment: COMMENT_FRAGMENT,
from: comment,
});
// ... render comment details
}
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.
import { COMMENT_FRAGMENT } from "./Comment";
export const POST_DETAILS_FRAGMENT = gql`
fragment PostDetailsFragment on Post {
title
shortDescription
topComment {
id
...CommentFragment
}
}
${COMMENT_FRAGMENT}
`;
export default function PostDetails({ post }) {
const { data, complete } = useFragment({
fragment: POST_DETAILS_FRAGMENT,
from: post,
fragmentName: "PostDetailsFragment",
});
// complete check omitted for brevity
return (
<section>
<h1>{data.title}</h1>
<p>{data.shortDescription}</p>
<Comment comment={data.topComment} />
</section>
);
}
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.
{
"__typename": "Post",
"title": "The Amazing Adventures of Data Masking",
"shortDescription": "In this article we dive into...",
"topComment": {
"__typename": "Comment",
"id": "1"
}
}
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.
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.
For more information about mutations, visit the mutations page.
// data is masked
const [mutate, { data }] = useMutation(MUTATION, {
onCompleted: (data) => {
// data is masked
},
update: (cache, { data }) => {
// data is unmasked
},
refetchQueries: ({ data }) => {
// data is unmasked
},
updateQueries: {
ExampleQuery: (previous, { mutationResult }) => {
// mutationResult is unmasked
},
},
});
async function runMutation() {
const { data } = await mutate();
// data is masked
}
// data is masked
const { data } = await client.mutate({
update: (cache, { data }) => {
// data is unmasked
},
refetchQueries: ({ data }) => {
// data is unmasked
},
updateQueries: {
ExampleQuery: (previous, { mutationResult }) => {
// mutationResult is unmasked
},
},
});
For more information about subscriptions, visit the subscriptions page.
function MyComponent() {
// data is masked
const { data } = useSubscription(SUBSCRIPTION, {
onData: ({ data }) => {
// data is unmasked
},
});
}
const observable = client.subscribe({ query: SUBSCRIPTION });
observable.subscribe({
next: ({ data }) => {
// data is masked
},
});
const { subscribeToMore } = useQuery(QUERY);
function startSubscription() {
subscribeToMore({
document: SUBSCRIPTION,
updateQuery: (queryData, { subscriptionData }) => {
// queryData is unmasked
// subscriptionData is unmasked
},
});
}
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.
query GetPosts {
posts {
id
...PostFragment @unmask
}
}
Only fragments marked with @unmask will unmask the results. Fragments not marked with @unmask will remain masked.
query GetPosts {
posts {
id
...PostFragment @unmask
# This data remains masked
...PostDetailsFragment
}
}
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.
query GetCurrentUser {
currentUser {
id
...ProfileFragment
}
}
fragment ProfileFragment on User {
name
age
}
The type definition for the query might resemble the following:
type GetCurrentUserQuery = {
currentUser: {
__typename: "User";
id: string;
name: string;
age: number;
} | null;
};
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.
type GetCurrentUserQuery = {
currentUser: {
__typename: "User";
id: string;
// omitted: additional internal metadata
} | null;
};
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.
typescript-operations pluginAdd the following configuration to your GraphQL Codegen config.
const config: CodegenConfig = {
// ...
generates: {
"path/to/types.ts": {
plugins: ["typescript-operations"],
config: {
// ...
inlineFragmentTypes: "mask",
customDirectives: {
apolloUnmask: true,
},
},
},
},
};
client-presetYou 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 useFragment function, with Apollo Client's useFragment hook.
Turn off Fragment Masking in your GraphQL Codegen config, along with these additions:
const config: CodegenConfig = {
// ...
generates: {
"path/to/gql/": {
preset: "client",
presetConfig: {
// ...
// disables the incompatible GraphQL Codegen fragment masking feature
fragmentMasking: false,
},
config: {
customDirectives: {
apolloUnmask: true
}
inlineFragmentTypes: "mask",
}
}
}
}
Enable data masking in Apollo Client.
By default, Apollo Client makes no modification to the operation types provided to its APIs, 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.
To use GraphQL Codegen's masking format with your operation types, you need to tell Apollo Client to use the associated GraphQL Codegen masking types. You do this by defining a type override that uses the GraphQL Codegen masking format.
Create a TypeScript file that will be used to modify the TypeOverrides interface. Extend TypeOverrides with the GraphQLCodegenDataMasking.TypeOverrides interface.
// This import is necessary to ensure all Apollo Client imports
// are still available to the rest of the application.
import "@apollo/client";
import type { GraphQLCodegenDataMasking } from "@apollo/client/masking";
declare module "@apollo/client" {
interface TypeOverrides extends GraphQLCodegenDataMasking.TypeOverrides {}
}
This example uses apollo-client.d.ts as the file name to make it easily identifiable. You can give this file any name.
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:
Apollo Client provides the FragmentType helper type for this purpose. As an example, let's use the PostDetails fragment from previous sections.
import type { FragmentType } from "@apollo/client";
import type { PostDetailsFragment } from "./path/to/gql/types.ts";
export const POST_DETAILS_FRAGMENT: TypedDocumentNode<PostDetailsFragment> = gql`
fragment PostDetailsFragment on Post {
title
shortDescription
}
`;
interface PostDetailsProps {
post: FragmentType<PostDetailsFragment>;
}
function PostDetails({ post }: PostDetailsProps) {
const { data } = useFragment({
fragment: POST_DETAILS_FRAGMENT,
from: post,
});
// ...
}
Using properties from the post prop instead of the data from useFragment results in a TypeScript error similar to the following:
function PostDetails({ post }: PostDetailsProps) {
// ...
post.title;
// ❌ Property 'title' does not exist on type '{ " $fragmentRefs"?: { PostDetailsFragment: PostDetailsFragment; } | undefined; }'
}
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.
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
shortDescription
}
}
`;
export default function Posts() {
// ...
return (
<div>
{allPosts.map((post) => (
<PostDetails key={post.id} post={post} />
// ❌ Type '{ __typename: "Post"; id: string; title: string; shortDescription: string; }' has no properties in common with type '{ " $fragmentRefs"?: { PostDetailsFragment: PostDetailsFragment; } | undefined; }'.
))}
</div>
);
}
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.
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.
import { Unmasked } from "@apollo/client";
type QueryType = {
currentUser: {
__typename: "User";
id: string;
name: string;
} & { " $fragmentRefs"?: { UserFragment: UserFragment } };
};
type UserFragment = {
__typename: "User";
age: number | null;
} & { " $fragmentName"?: "UserFragment" };
type UnmaskedQueryType = Unmasked<QueryType>;
// ^? type UnmaskedQueryType = {
// currentUser: {
// __typename: "User";
// id: string;
// name: string;
// age: number | null;
// }
// }
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.
@unmask directive to all fragment spreadsBefore enabling the dataMasking flag in the client, it is wise to ensure that your components continue to receive full results to avoid breakages. You can use the @unmask directive to handle this.
query GetPost($id) {
post(id: $id) {
id
...PostDetails @unmask(mode: "migrate")
}
}
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 repository
git clone https://github.com/apollographql/apollo-client.git
Install dependencies in apollo-client
npm install
Run the codemod via jscodeshift against your codebase.
npx jscodeshift -t ../path/to/apollo-client/scripts/codemods/data-masking/unmask.ts --extensions ts --parser ts ./src/**/*.ts --mode migrate
The codemod supports .js, .jsx, .ts, .tsx, .graphql, and .gql files. For .graphql and .gql files, explicitly specify the file extensions.
npx jscodeshift -t ../path/to/apollo-client/scripts/codemods/data-masking/unmask.ts --extensions graphql ./src/**/*.graphql --mode migrate
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.
npx jscodeshift ... --tag myGql
dataMaskingWith fragments unmasked, it is safe to enable data masking in your application. Add the dataMasking option to your client instance to enable it.
new ApolloClient({
dataMasking: true,
// ...
});
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.
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.
useFragmentWith 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.
function PostDetails({ post }) {
const { data, complete } = useFragment({
fragment: POST_DETAILS_FRAGMENT,
from: post,
});
// ... use `data` instead of `post`
}
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.
query GetPosts {
posts {
id
- ...PostDetails @unmask(mode: "migrate")
+ ...PostDetails
}
}
Repeat this process until all @unmask directives have been removed from your codebase.
Congratulations 🎉! Your application is now using data masking everywhere 😎.
If you are using the data masking types generated from GraphQL Codegen, you can safely skip this section.
</Note>Apollo Client provides an integration with GraphQL Codegen's fragment masking output and orients its masking utility types around this format. If you aren't using GraphQL Codegen to generate your types, or your types are generated in a different format, the type implementations built into Apollo Client might be incompatible which results in inaccurate types.
You can use a technique called higher-kinded types (HKT) to provide your own type implementations for Apollo Client's masking types. You can think of higher-kinded types as a way to define types and interfaces with generics that can be filled in by Apollo Client internals at a later time. Passing around un-evaluated types is otherwise not possible in TypeScript.
Let's add our own type overrides for the MaybeMasked and Unmasked utility types.
The MaybeMasked type is used throughout Apollo Client to return the masked or unmasked type definition for a given type, assuming they have data masking types enabled and are maskable. Otherwise, it returns the unmasked type that is passed in.
The Unmasked type unwraps a masked type to its full result type and is used throughout Apollo Client where the full data type is needed for a given API (e.g. client.writeQuery).
For this example, we'll assume the data masking types are enabled and the operation types are generated using two variations:
Data masked types are generated with a __masked virtual property. Its value is the operation type with any masked fields removed from the type.
type MaskedQuery = {
// The masked variant of the operation type is provided under the
// `__masked` virtual property
__masked?: { user: { __typename: "User"; id: number } };
// The full result type includes all other fields in the type
user: { __typename: "User"; id: number; name: string };
};
Unmasked types are generated as their full result type with no __masked virtual property applied to them.
type UnmaskedQuery = {
user: { __typename: "User"; id: number; name: string };
};
This is a hypothetical format that doesn't exist in Apollo Client or any known code generation tool. This format is used specifically for this example to illustrate how to provide type overrides to Apollo Client.
</Note>First, let's define our custom implementation of the MaybeMasked type. The implementation works by checking if the __masking virtual property exists on the type. If so, it returns the value on the __masked property as the type, otherwise it returns the input type unmodified.
type MaybeMasked<TData> =
TData extends { __masked?: infer TMaskedData } ? TMaskedData : TData;
Now let's provide an implementation for the Unmasked type that works in contrast to MaybeMasked by returning the unwrapped full result type. The implementation works by removing the __masked virtual property on the input type. This can be accomplished using the built-in Omit type.
type MaybeMasked<TData> =
TData extends { __masked?: infer TMaskedData } ? TMaskedData : TData;
type Unmasked<TData> = Omit<TData, "__masked">;
Now that our custom type implementations are in place, we need to define higher-kinded types for each of these custom types. This provides the bridge needed by Apollo Client to use our custom type implementations. This is done by extending the HKT interface exported by @apollo/client/utilities.
Let's provide HKTs for our MaybeMasked and Unmasked types. We'll put these in the same file as our type implementations.
import { HKT } from "@apollo/client/utilities";
type MaybeMasked<TData> =
TData extends { __masked?: infer TMaskedData } ? TMaskedData : TData;
type Unmasked<TData> = Omit<TData, "__masked">;
export interface MaybeMaskedHKT extends HKT {
arg1: unknown; // TData
return: MaybeMasked<this["arg1"]>;
}
export interface UnmaskedHKT extends HKT {
arg1: unknown; // TData
return: Unmasked<this["arg1"]>;
}
With our HKT types in place, we now need to tell Apollo Client about them.
Apollo Client uses declaration merging to provide a hook point for overridable types. The TypeOverrides interface exported by the @apollo/client package is used for this purpose. Each property in the TypeOverrides interface corresponds to an overridable type in Apollo Client.
Let's add type overrides for our custom type implementations. Create a TypeScript file and define a TypeOverrides interface for the @apollo/client module.
// This import is necessary to ensure all Apollo Client imports
// are still available to the rest of the application.
import "@apollo/client";
declare module "@apollo/client" {
export interface TypeOverrides {
// Type overrides will go here
}
}
Now we'll import our HKT types and add them as keys in the TypeOverrides interface.
// This import is necessary to ensure all Apollo Client imports
// are still available to the rest of the application.
import "@apollo/client";
import { MaybeMaskedHKT, UnmaskedHKT } from "./masked-types.ts";
declare module "@apollo/client" {
export interface TypeOverrides {
MaybeMasked: MaybeMaskedHKT;
Unmasked: UnmaskedHKT;
}
}
And that's it! Now when Apollo Client uses the MaybeMasked or Unmasked types in its APIs, our custom implementation will be used instead 🎉.
The following masking utility types are available to override:
FragmentType<TFragmentData> - Type used with fragments to ensure parent objects contain the fragment spread from the type.MaybeMasked<TData> - Conditionally returns TData as either its masked type or unmasked typeUnmasked<TData> - Unwraps TData into the full result typeFor more information about other overridable types in Apollo Client, see the TypeScript guide.