Docs
Launch GraphOS Studio

Fragments

Share fields between operations


A

is a piece of logic that can be shared between multiple queries and .

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

fragment NameParts on Person {
firstName
lastName
}

Every includes a subset of the that belong to its associated type. In the above example, the Person type must declare firstName and lastName for the NameParts to be valid.

We can now include the NameParts in any number of queries and that refer to Person objects, like so:

query GetPerson {
people(id: "7") {
...NameParts
avatar(size: LARGE)
}
}

You precede an included with three periods (...), much like JavaScript

.

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

query GetPerson {
people(id: "7") {
firstName
lastName
avatar(size: LARGE)
}
}

If we later change which are included in the NameParts , we automatically change which are included in that use the . This reduces the effort required to keep consistent across a set of .

Example usage

Let's say we have a blog application that executes several related to comments (submitting a comment, fetching a post's comments, etc.). These operations probably all include certain of a Comment type.

To specify this core set of , we can define a on the Comment type, like so:

fragments.js
import { gql } from '@apollo/client';
export const CORE_COMMENT_FIELDS = gql`
fragment CoreCommentFields on Comment {
id
postedBy {
username
displayName
}
createdAt
content
}
`;

You can declare in any file of your application. The example above exports the from a fragments.js file.

We can then include the CoreCommentFields in a like so:

PostDetails.jsx
import { gql } from '@apollo/client';
import { CORE_COMMENT_FIELDS } from './fragments';
export const GET_POST_DETAILS = gql`
${CORE_COMMENT_FIELDS}
query CommentsForPost($postId: ID!) {
post(postId: $postId) {
title
body
author
comments {
...CoreCommentFields
}
}
}
`;
// ...PostDetails component definition...
  • We first import CORE_COMMENT_FIELDS because it's declared in another file.
  • We add our definition to the GET_POST_DETAILS gql template literal via a placeholder (${CORE_COMMENT_FIELDS})
  • We include the CoreCommentFields in our with standard ... notation.

Registering named fragments using createFragmentRegistry

Starting in 3.7, can be registered with your InMemoryCache so that they can be referred to by name in any or InMemoryCache (such as cache.readFragment, cache.readQuery and cache.watch) without needing to interpolate their declarations.

Let's look at an example in React.

index.js
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 spread inside of the GetItemList .

ItemList.jsx
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>
);
}

Overriding registered fragments with local versions

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

index.js
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 declared in ItemList.jsx takes precedence over the ExtraFields originally registered with the InMemoryCache. Thus, its definition will be used when the ExtraFields spread is parsed inside of the registered ItemFragment only when GetItemList query is executed, because explicit definitions take precedence over registered .

ItemList.jsx
const listQuery = gql`
query GetItemList {
list {
...ItemFragment
}
}
fragment ExtraFields on Item {
createdBy
}
`;
function ToDoList() {
const { data } = useQuery(listQuery);
return (
<ol>
{data?.list.map((item) => (
{/* `createdBy` exists on the returned items, `isCompleted` does not */}
<Item key={item.id} text={item.text} author={item.createdBy} />
))}
</ol>
);
}

Colocating fragments

The tree-like structure of a response resembles the hierarchy of a frontend's rendered components. Because of this similarity, you can use to split logic up between components, so that each component requests exactly the that it uses. This helps you make your component logic more succinct.

Consider the following view hierarchy for an app:

FeedPage
└── Feed
└── FeedEntry
├── EntryInfo
└── VoteButtons

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

Creating colocated fragments

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

VoteButtons.jsx
VoteButtons.fragments = {
entry: gql`
fragment VoteButtonsFragment on FeedEntry {
score
vote {
choice
}
}
`,
};

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

FeedEntry.jsx
FeedEntry.fragments = {
entry: gql`
fragment FeedEntryFragment on FeedEntry {
commentCount
repository {
full_name
html_url
owner {
avatar_url
}
}
...VoteButtonsFragment
...EntryInfoFragment
}
${VoteButtons.fragments.entry}
${EntryInfo.fragments.entry}
`,
};

There's nothing special about the naming of VoteButtons.fragments.entry or EntryInfo.fragments.entry. Any naming convention works as long as you can retrieve a component's given the component.

Importing fragments when using Webpack

When loading .graphql files with

, we can include using import statements. For example:

#import "./someFragment.graphql"

This makes the contents of someFragment.graphql available to the current file. See the

section for additional details.

Using fragments with unions and interfaces

You can define on

.

Here's an example of a that includes three in-line :

query AllCharacters {
all_characters {
... on Character {
name
}
... on Jedi {
side
}
... on Droid {
model
}
}
}

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

However, for this to work, your 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 can pass a possibleTypes option when you initialize your InMemoryCache.

Defining possibleTypes manually

The possibleTypes option is available in 3.0 and later.

You can pass a 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 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 consider

.

Generating possibleTypes automatically

The following example script translates a 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,
});

useFragment

The useFragment hook represents a lightweight live binding into the Cache. It enables Apollo Client to broadcast specific 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 (

). As a result, the component reading the data via useFragment is still subscribed to all changes in the data, but receives updates only when that 's specific data change.

Note: this hook was introduced in 3.7.0 as experimental but stabilized in 3.8.0. In 3.7.x and 3.8.0-alpha.x releases, this hook is exported as useFragment_experimental. Starting with 3.8.0-beta.0 and greater the _experimental suffix was removed in its named export.

Example

Given the following definition:

const ItemFragment = 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 selected on the named ItemFragment by spreading ItemFragment inside of list in ListQuery.

const listQuery = gql`
query GetItemList {
list {
id
...ItemFragment
}
}
${ItemFragment}
`;
function List() {
const { loading, data } = useQuery(listQuery);
return (
<ol>
{data?.list.map(item => (
<Item key={item.id} id={item.id}/>
))}
</ol>
);
}

Note: Instead of interpolating within each , we can use 's createFragmentRegistry method to pre-register named with our InMemoryCache. This allows to include the definitions for registered in the sent over the network before the request is sent. For more information, see

.

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

function Item(props: { id: number }) {
const { complete, data } = useFragment({
fragment: ItemFragment,
fragmentName: "ItemFragment",
from: {
__typename: "Item",
id: props.id,
},
});
return <li>{complete ? data.text : "incomplete"}</li>;
}

useFragment can be used in combination with the @nonreactive in cases where list items should react to individual cache updates without rerendering the entire list. For more information, see the

.

Previous
Subscriptions
Next
Directives
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company