Prepared Docs

Prepared Docs

⌘K

    Loading navigation…

User

  1. Working With Data
  2. Graphql Fragments

GraphQL Fragments

Component-owned fragments and Apollo data masking

A fragment is the data contract of one component. The component declares the fields it reads in its own fragment, the route's query spreads that fragment, and Apollo's data masking hands the component only those fields. A component can't read data it didn't ask for, so a parent's query can't quietly start to depend on a child's fields, or the other way around.

Masking is on everywhere: createApolloClient sets dataMasking: true, and codegen emits masked types (inlineFragmentTypes: "mask"). The generated types and the runtime data agree, so a field that isn't in a component's fragment is a type error and also undefined at runtime.

The rules

  1. One component, one fragment, named Component_prop. ChatroomListItem_chatroom is the fragment of the ChatroomListItem component for its chatroom prop. The pattern is enforced by pnpm lint:graphql.
  2. A fragment is for colocation, not reuse. If two components need the same fields, each declares its own fragment, even when the selections are identical. Sharing one fragment ties the two components together: a field one of them needs is fetched for both, and removing a field that only one reads is never safe.
  3. A parent spreads only its direct children's fragments. If ChatroomListItem renders ChatroomLabel, then ChatroomListItem_chatroom spreads ...ChatroomLabel_chatroom and the route's query does not. Spreading a grandchild's fragment makes the parent depend on a component it doesn't render.
  4. One query per route, at the query root. The route (or a remote's top-level view, or shell chrome) calls useQuery. Every component below it gets its data from its fragment and reads it with useFragment. See Query roots.
  5. Props are FragmentType, not data. A component takes the masked reference its parent passes and calls useFragment to open it.
  6. @unmask is an escape hatch and needs a reason. See @unmask.

Define, spread, read

Each component gets a fragment in a fragments.graphql next to it:

The query root spreads its direct child's fragment. It selects id itself, because Apollo needs it to find the entity again and to key the list:

There is no #import line: codegen and lint read every document, so a spread of a fragment defined in another file just works.

pnpm codegen writes a …FragmentDoc (the document) and a …Fragment type (the type of the fragment's own fields) named after each fragment, next to the .graphql file. For ChatroomListItem_chatroom those are, e.g., ChatroomListItemChatroomFragmentDoc and ChatroomListItemChatroomFragment. A component takes a FragmentType for its prop and opens it with useFragment:

data holds this component's own fields plus a reference to ChatroomLabel_chatroom, which ChatroomLabel opens in the same way. The route is the only component that calls a query hook:

useFragment and complete

useFragment reads the fragment from the cache. It never makes a request, and it subscribes to that slice of the cache, so the component re-renders when its own fields change and not when something else in the cache does. It returns { complete, data, missing }:

  • complete is true when every field of the fragment is in the cache. After the check, data is typed as the whole fragment. Before it, data may be partial.
  • A complete: false that you don't expect usually means the parent didn't select the entity's id, or its cache keyFields don't match. Don't paper over it with a cast.
  • useSuspenseFragment is the drop-in alternative that suspends while the data is incomplete, so there is no complete to check. It pairs with @defer: don't defer the key fields, or the component can suspend indefinitely.

Reading by id

When a component has an id and no parent object (a map marker for a chatroom id, a row that a subscription updates), give useFragment the entity to read:

The fragment is still the component's own. Nothing else about the rules changes: the entity has to be in the cache because some route's query put it there.

Helper functions

ChatroomListItemChatroomFragment is the type of the fragment's own fields, without the references to child fragments. Use it for a helper function or a custom hook that takes a component's data after useFragment has opened it. Don't use it as a prop type: a prop is the FragmentType reference the parent passes.

Query roots

A query root is the component that owns a screen's request:

  • a route component under apps/*/src/routes/
  • shell chrome under apps/*/src/core/
  • a remote's top-level view or page: an exposed module (the files in remotes/*/exposes/, or an entry such as View.tsx in remotes/*/src/)

Everything below a query root declares a fragment and reads it with useFragment. A component that calls useQuery itself works only where it is mounted with no parent query, and it makes a request of its own for data that its parent was already going to fetch. The repo/graphql review agent asks about a query hook added in a component that is clearly a leaf from its path alone: a file under a components/ directory (other than exposes/), or one named …Card, …Item, …Row, …Badge, …Popup, …Button, …Label, …Tile, or …Pin. It can't see the render tree, so a leaf with another name is caught in review.

Tests, stories, and mocks may call a query hook wherever they need to. A custom hook isn't a component, so the thin-wrapper rule governs it instead.

@unmask

@unmask makes a fragment's fields visible to the parent again, as if there were no masking:

It couples the two components, which is what masking exists to prevent, so treat it as an escape hatch:

  • First try adding the fields the parent needs to its own fragment or query.
  • When you do use it, say why in a # comment on the line or the line above. The repo/graphql review agent asks about an @unmask with no comment there; whether the reason is a good one is for the reviewer.
  • @unmask(mode: "migrate") keeps masking on and logs a development warning each time code reads a field that masking would hide. It is for moving existing code to fragments, not for new code.

Mutations, subscriptions, and the cache

Masking applies to every request-based API: useQuery, useMutation, useSubscription, client.query, and client.mutate. A mutation result that spreads a fragment is masked in the same way.

The cache APIs are never masked. cache.readFragment, cache.writeFragment, cache.updateQuery, and cache.modify see and write whole objects, so a subscription handler can update a fragment's fields directly (see Pub/Sub).

Polymorphic fragments

A fragment on an interface or union selects the shared fields and then the fields of each member. The component that owns it handles each case:

Query splitting

When a component's fragment feels too large, extract focused child fragments (one per component that reads them) and spread them back. Do not give each component its own useQuery:

  • Many requests for one entity. Each mounted component makes a separate round trip, and the resolver for chatroom(id:) runs once per query.
  • Apollo's deduplication doesn't help. It only merges identical documents with identical variables. Different documents for the same chatroom(id:) are different requests.
  • Fields arrive at different times, which gives inconsistent render states, and a real-time update has to reach every one of the queries.

The fix is the structure above: ChatbotStatus_chatroom and LanguageBanner_chatroom fragments, each read with useFragment by its component, spread by the component that renders them, with the route's single query at the top. Breaking a fragment up this way doesn't shrink the payload by itself. It makes each field's owner obvious, so a field is removed when its component stops reading it.

Migrating from query result types

Extracting a type from a query result with indexed access ties a component to the query's shape. Give the component a fragment instead:

The oxlint rule no-result-type-indexing rejects the indexed form for generated *Query, *Mutation, and *Subscription types.

  1. Create AudioCollectorDevice_device in a fragments.graphql next to the component.
  2. Spread it in the parent's fragment or query, in place of the inline fields.
  3. Change the prop to FragmentType<typeof …FragmentDoc> and open it with useFragment.
  4. Run pnpm codegen. TypeScript now shows every place that read a field the fragment doesn't select.

Best practices

✅ Do

  • Name a fragment after its component: ChatroomListItem_chatroom, UserCard_user.
  • Colocate it: put it in a fragments.graphql in the component's directory.
  • Select id on every object whose fragment is read by a child. pnpm lint:graphql requires it where the type has one.
  • Check complete before using the data from useFragment.
  • Remove a field when its component stops reading it. Nothing in lint finds an unused field or fragment (graphql-eslint can't see TypeScript), so review asks.

❌ Don't

  • Spread a grandchild's fragment into a query or fragment. Spread the child's, and let the child spread its own.
  • Share one fragment between components. Give each component its own, even if they match today.
  • Type a prop with the fragment's data type. The prop is a FragmentType.
  • Call useQuery below the query root. Declare a fragment and spread it into the root's query.
  • Cast away a missing field. If a component needs a field its fragment doesn't select, add the field to the fragment.
  • Split a query per component to avoid a large fragment. Extract child fragments instead.

Testing

renderWithApollo builds its client with createApolloClient, so tests run with masking on, the same as the app. The factories and mock handlers return whole objects; the client masks them on the way in, so a component test sees exactly what the component would see in production. Render the query root and assert on the child's output, or give a child a reference from a query result.

Common pitfalls

Fragment import paths

Import from the .graphql file. Vite resolves it to the generated .graphql.ts:

Fragment names are unique across the repository

The Component_prop pattern keeps names unique in practice, because component names are. Generic names like Data or ChatroomData will collide, and the pattern rejects them.

Masked types need the augmentation

The FragmentType and MaybeMasked types only mask when @prepared911/data-gql is part of the TypeScript program. Every generated *.graphql.ts imports it, so a package that uses a generated document gets it automatically. If a package uses @apollo/client without any generated document and reads fragment data, import @prepared911/data-gql once so the types apply.

Sources

  • GraphQL specification, "Update description of Fragments to emphasize evolving data needs" (graphql-spec#1193, merged February 2026): fragments exist so each data-consuming component declares its own needs, not to share selections.
  • Apollo Client, Fragments and data masking and @unmask.
  • Janette Cheng, How To Use Fragments (They're Not for Re-use!), GraphQLConf 2025.

For day-to-day workflow, debugging, and troubleshooting, see the Development Workflow guide.

Previous

Working with data / GraphQL Colocation

Next

Working with data / GraphQL Development Workflow

On this page

The rules
Define, spread, read
useFragment and complete
Reading by id
Helper functions
Query roots
@unmask
Mutations, subscriptions, and the cache
Polymorphic fragments
Query splitting
Migrating from query result types
Best practices
✅ Do
❌ Don't
Testing
Common pitfalls
Fragment import paths
Fragment names are unique across the repository
Masked types need the augmentation
Sources