Selection Sets That Drift Apart
Selection Sets That Drift Apart
Two screens need the same nine fields on User, so each query declares them:
query Profile { user(id: $id) { id name avatarUrl bio handle joinedAt location } }
query Sidebar { me { id name avatarUrl bio handle } }
The sidebar shipped first and stops at avatarUrl. Later the profile adds handle — and then a designer notices the sidebar header renders nothing, because a second copy of the field list nobody remembered is still missing it. Duplication across documents has no compiler watching it, so drift is the default outcome rather than an accident.
A named fragment collapses the copies:
fragment UserCard on User {
id
name
avatarUrl
bio
handle
}
query Profile($id: ID!) {
user(id: $id) { ...UserCard joinedAt location }
}
query Sidebar { me { ...UserCard } }
handle now has exactly one declaration, and every consumer picks it up on the next request. The fragment is still just a selection set: execution cost, resolver calls, and response shape are identical to writing those fields inline, because the server expands spreads before it runs anything. The win is purely in maintenance — one place to change, many places to benefit.
The same argument applies across a team. Once a field list is written twice, nobody can tell which copy is authoritative: code review approves one, a test exercises the other, and the fragment is the only structure that gives those fields a single owner.
Fragments also shrink reviews. Adding a field to UserCard is one line in one file; without fragments the same change is a diff in every query that renders a user, each needing its own reasoning about whether that screen wants the field at all — and that reasoning is exactly what gets skipped under time pressure.
Declare Once, Spread Anywhere
Declare Once, Spread Anywhere
Spreads work inside any selection set, at any depth, including inside other fragments:
query Dashboard($id: ID!) {
user(id: $id) {
...UserCard
posts(first: 5) {
...PostPreview
author { ...UserCard } # same fragment, different path
}
}
}
Four validation rules are the ones you will actually hit:
- The type condition must fit. Spreading
fragment PostPreview on Postwhere the selection's type isUserfails withFragment cannot be spread here. A fragment declared on an interface or union may be spread wherever one of its members appears, which is what makes polymorphic reuse work. - Fragment names are unique per document. Two definitions named
UserCardin one operation collide at validation, not at execution. - Every defined fragment must be used. An unused definition is a validation error, so dead fragments cannot survive a refactor quietly.
- No fragment may reach itself. A fragment that spreads into itself through a nested field fails the no-cycles rule — see circular queries and cycle guards for why that rule exists.
Because spreads merge into the surrounding selection, fragments leave no trace in the JSON: the response contains the union of the fields, under the parent's own key. That property is what lets UserCard appear at the top level of one query and three levels deep in another while the client keeps reading a plain object.
One practical habit: fragments describe UI pieces ("what a user card renders"), not whole types. A fragment that selects everything on User reintroduces over-fetching under a new name.
Two consequences of merging are worth knowing before you refactor. First, fragments in the same selection set are checked by the ordinary field-merging rule: if two of them select posts with different limits, the document fails validation exactly as inline selections would, so shared fields belong in one definition rather than two overlapping ones. Second, a fragment can sit inside an inline fragment, which is how a polymorphic query reuses a concrete-type selection — see inline fragments.
Where Fragments Live in a Real Client
Where Fragments Live in a Real Client
Two ownership models, both running in production codebases:
| Model | Shape | Wins when |
|---|---|---|
| Colocated | fragment defined beside the component that renders it | many teams; screens evolve independently |
| Centralized | one fragments/user.graphql imported everywhere |
small surface; the API team reviews what is exposed |
Whichever you pick, tooling reads the same documents. Codegen turns each fragment into a TypeScript type containing exactly the selected fields, so a UI change and its type change travel together — see TypeScript codegen. Normalized caches use fragment definitions when deciding whether a stored record can satisfy a query, which is why the same fragment drives rendering and cache reads — see response caching strategy.
When fragments stop helping:
- A three-field fragment used once is indirection, not reuse — inline it.
- One enormous fragment spread everywhere over-fetches again; scope fragments to components.
- Duplicating a fragment per variation is the drift problem returning. Conditional presence belongs to directives — see include and skip directives.
- Renaming or deleting a fragment changes generated types across every consumer; treat the fragment name as an interface.
Interview angle: explain why GraphQL's schema validation does not catch duplicated selection sets — validation checks each document in isolation, so two documents with divergent copies of the same selection both pass. Reuse is a project-level discipline, and fragments are the mechanism.
A naming rule keeps the strategy honest: name fragments after the component that renders them (UserCard), never after the type alone. Type-named fragments attract fields until they become the everything-fragment that both ownership models warn about, at which point every consumer over-fetches together.
Practice Problems
Two operations select overlapping user fields and have already drifted apart. Define one fragment that satisfies both screens, rewrite the queries to spread it, and explain what stays identical after the change: resolver calls, response shape, or both.
Example:
Input: query Profile { user(id: "7") { id name avatarUrl bio handle joinedAt } } query Sidebar { me { id name avatarUrl bio } } # missing handle — drifted
Output: fragment UserCard on User { id name avatarUrl bio handle } query Profile($id: ID!) { user(id: $id) { ...UserCard joinedAt } } query Sidebar { me { ...UserCard } }
handle now has one declaration, so both screens receive it. Fields shared by both queries live in the fragment; the profile-only field stays in its own selection.
Optimal Solution — O(1) time, O(f) space
Group fields by the UI piece that renders them, not by the type that owns them; keep screen-specific fields outside the fragment.
fragment UserCard on User {
id
name
avatarUrl
bio
handle
}
query Profile($id: ID!) {
user(id: $id) {
...UserCard
joinedAt
}
}
query Sidebar {
me {
...UserCard
}
} Edge Cases:
- Spreading the fragment where the type is Post — rejected with "Fragment cannot be spread here"
- Defining UserCard twice in one document — duplicate name validation error
- An unused fragment definition — NoUnusedFragments rejects the document
Quiz
1. fragment PostPreview on Post is spread inside a selection of type User. What happens?
2. What does a fragment change about the response JSON?
3. A document defines fragment Unused on User but never spreads it. What does the server do?
4. Do fragments reduce the number of resolver calls a query makes?
Flashcards
Question
Named fragment syntax
Click to reveal answer
Answer
fragment Name on Type { fields } — applied with ...Name inside any selection set of a compatible type.
Question
Fragment validation rules
Click to reveal answer
Answer
Type condition must fit; names unique per document; every fragment used; no cycles.
Question
Fragments and the response JSON
Click to reveal answer
Answer
Spreads vanish at execution — fields merge into the parent selection with no wrapper or key.
Question
Fragment scope rule
Click to reveal answer
Answer
Scope fragments to UI pieces, not whole types: an everything-fragment just over-fetches under a new name.
Question
Colocated vs centralized fragments
Click to reveal answer
Answer
Colocated: evolves with the component. Centralized: one reviewable surface. Both are read by codegen and caches.
Revision Notes
Key Takeaways
- 1. Duplicated selection sets drift because validation checks each document in isolation
- 2. A fragment is a named selection set on one type, spread with ... and expanded before execution
- 3. Four validation rules cover fit, uniqueness, usage, and cycles
- 4. Fragments shape maintenance and tooling, not execution cost or response structure
Interview Tips
- • State precisely what a fragment changes (document reuse, generated types, cache reads) and what it does not (resolver calls, JSON shape)
- • Walk the validation errors in order: wrong type condition, duplicate name, unused fragment, cycle
- • Discuss fragment ownership as a team-scale decision, not a syntax preference — colocate or centralize with a reason
Cheat Sheet
Named Fragments Cheat Sheet
fragment UserCard on User { id name avatarUrl bio handle }
query Profile($id: ID!) { user(id: $id) { ...UserCard joinedAt } }
query Sidebar { me { ...UserCard } }
- Spread anywhere a selection set is allowed, including inside other fragments
- Validation: type condition fits · unique names · all used · no cycles
- Response = merged fields, no fragment key in the JSON
- Scope to UI pieces; one fragment per type invites over-fetching
- Aliases go on the field:
viewer: user { ...UserCard } - Codegen types and cache reads both derive from the fragment definition