Axon 911 Frontend Docs
    Loading navigation…
U
User
Switch to light mode
  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 query above it spreads that fragment, and Apollo's data masking hands the component only those fields. Masking is on everywhere: createApolloClient sets dataMasking: true, and codegen emits masked types (inlineFragmentTypes: "mask"). Reading a field the fragment didn't select is therefore a type error, and the field is undefined at runtime. This page is the one home for our fragment rules; for how useFragment and masking work in general, see Apollo's fragments docs.
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. Never import one component's fragment into another component.
  3. A parent spreads only its direct children's fragments. If ChatroomListItem renders ChatroomLabel, then ChatroomListItem_chatroom spreads ...ChatroomLabel_chatroom and the page's query does not.
  4. Fragments by default; an own query when it is justified. A component below a query gets its data from its fragment and reads it with useFragment. It runs its own query only in the cases listed in When a component runs its own query.
  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.
  7. A fragment is on a type the cache can identify. See Types without an identity.
Define, spread, read
Each component gets a fragment in a fragments.graphql next to it:
The parent's query 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. Always pass fragmentName (the fragment's Component_prop name) to useFragment, readFragment, writeFragment and watchFragment. A fragment document that spreads a child's fragment holds more than one definition, and Apollo then requires fragmentName, so passing it from the start keeps the call working when someone later adds ...Child:
data holds this component's own fields plus a reference to ChatroomLabel_chatroom, which ChatroomLabel opens in the same way. Always pass fragmentName, as Apollo's own examples do, to useFragment, useSuspenseFragment, readFragment, writeFragment, and watchFragment, even when the fragment spreads nothing today; pnpm lint:graphql reports a call that leaves it out. The generated document of a fragment that spreads another holds the child's definition too, so it has more than one fragment in it, and Apollo throws fragmentName must be provided when there is not exactly 1 fragment when it can't tell which one to read. That means adding ...Child to a leaf's fragment later would break its parent's call at runtime, in a file the change never touched. Passing fragmentName works in every case. No lint rule checks it. 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 didn't spread the child's fragment, or the entity isn't in the cache. Don't paper over it with a cast. Why is useFragment incomplete? lists every cause.
  • 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 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.
When a component runs its own query
The default is a fragment. A component that renders data its parent's query already fetches, or could fetch at no extra cost (a list row, a card, a label, a section of a screen whose entity the screen already loads), declares a Component_prop fragment and reads it with useFragment. Apollo's bundled skill puts it the same way: a page should "generally" have one query, composed from the fragments of the components below it. A component may call a query hook itself when at least one of these is true:
  1. It mounts independently, so no ancestor's query could hold its data. A route, shell chrome, a remote's exposed view, a dashboard tile, or a drawer, dialog, popover or tab body that mounts on demand. A remote's view has to own its query: it deploys separately, so another app's query cannot spread its fragments. The exposes/ file is only the entry, because a document cannot sit there (see GraphQL Colocation).
  2. It needs a different freshness or lifetime from its parent. Catch-up on mount (cache-and-network with nextFetchPolicy: "cache-first"), a subscription-backed live panel inside a static screen, or polling. A fragment cannot refetch, so the part that needs fresher data has to own the query.
  3. It loads in response to a user action. useLazyQuery or useLoadableQuery on click, search or expand.
  4. Its data is unrelated to the parent's entity. A different root field. Putting it in the parent's query would only couple the two components.
A component with its own query is still masked, still reads its own fields, and still passes fragment references to its children. Every fragment rule above applies to it unchanged. Three patterns are worth avoiding. They are guidance only: no lint rule or review agent checks them, because deciding them needs the render tree and the other files.
  • Query splitting. A second document for an entity a mounted ancestor's query already selects. See Query splitting.
  • A query per list item. A .map that renders a component calling useQuery can make one request and one cache watch per row, and the cost grows with the list. Read a fragment, or fetch the rows in the list's query.
  • A query that waits on another query's loading state. If a child mounts only once its parent has data, its request starts after the parent's finishes, a waterfall (see Eliminating Waterfalls). Hoist it with useBackgroundQuery or merge the two queries, unless the child needs the parent's result as a variable.
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 disable comment on the line above: # eslint-disable-next-line no-restricted-syntax -- <why>. Lint reports every @unmask without one; 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.
Types without an identity
useFragment finds a component's object in the cache by its identity, which is id unless the cache is given keyFields for the type. A type with neither is stored inside its parent and has no entry of its own, so useFragment has nothing to look up: it returns complete: true with the fields missing, and Apollo logs "Could not identify object". A fragment on such a type is rejected by pnpm lint:graphql (axon/fragment-type-has-cache-identity), which reads the schema and packages/data-gql/src/cache-key-fields.json (the keyFields that createPreparedInMemoryCache is built with: TextCall, DispatchChatroomList and AliRecordParseResult, from the prepared911 dispatch cache; Participant is left out because its only key candidate is a display name). The file is validated when it loads: each type maps to a non-empty array of field names that exist on the type, and a typo fails lint with a message that names it.
  • A value object (an address, a chart's options, a location): the fragment of the entity that owns it selects its fields, and passes them to the child component as plain props. The child does not call useFragment for it.
  • A type whose fields really identify an object: add it to cache-key-fields.json ("TextCall": ["callId"]), which gives the cache the same keyFields. Do not add a type only to silence the rule.
  • @unmask does not clear the error, because the rule reports the fragment's definition and not its spreads.
On an interface or a union (see below), every type it can stand for must have an identity, because the object in the cache is one of them. The root types (Query, Mutation, Subscription) are exempt.
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). GraphQL Cache covers when to update the cache and when to refetch.
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 for data the page already loads:
  • 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 page'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.
Also
  • 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). The repo/graphql-unused-fields review agent checks the ones a PR adds to a .graphql file, but not a component that stops reading a field, so check that yourself.
  • Don't cast away a missing field. If a component needs a field its fragment doesn't select, add the field to the fragment.
  • Don't write a fragment on a type with no id and no keyFields. Select its fields in the owning entity's fragment and pass them down as props.
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 component that owns the query and assert on the child's output, or give a child a reference from a query result. To put fragment data in the cache directly, use Apollo's client.writeFragment({ fragment, fragmentName, data }) with from: { __typename, id }. A seedFragment helper (CORE-1305) is coming as a thin wrapper over it. The recipe for testing one fragment-owning component on its own is in Testing a Fragment-Owning Component on Its Own.
Common pitfalls
Fragment import paths
Import from the .graphql file. Vite resolves it to the generated .graphql.ts:
No fragment named … or fragmentName must be provided
The generated document of a fragment holds the fragment and every fragment it spreads, so useFragment, readFragment, watchFragment, and writeFragment find the children. If you see the first error, check that packages/data-gql/src/codegen.ts sets no documentMode (graphQLTag writes only the fragment's own definition) and re-run pnpm codegen. The second means a useFragment, useSuspenseFragment, readFragment, writeFragment or watchFragment call is missing fragmentName: pass the fragment's own name.
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
When a component runs its own query
@unmask
Types without an identity
Mutations, subscriptions, and the cache
Polymorphic fragments
Query splitting
Migrating from query result types
Also
Testing
Common pitfalls
Fragment import paths
No fragment named … or fragmentName must be provided
Fragment names are unique across the repository
Masked types need the augmentation
Sources