In Relay, why can't a parent component read the fields its child's useFragment fragment selects, even though they arrive in the same response?
answer
- one request, many readers
- a reference, not the data
- each hook reads its own selection
- an escape hatch exists, discouraged
basics
~20 sRelay's data masking: useFragment returns only the fields its own fragment selected. A spread child fragment reaches the parent as an opaque fragment reference, which the child passes to useFragment to read its fields from the store.
solid answer
~50 sA Relay query composes every component's fragment into one request, and the response is normalized into the store. Reading, though, is per fragment: `useFragment(fragment, ref)` returns the fields that fragment selected, plus references for the fragments it spreads. Where the article page spreads `...ArticleByline_article`, its data holds an opaque fragment reference typed `ArticleByline_article$key`, not the byline's fields, so reading the author's name there is `undefined` at runtime and a type error in TypeScript. That is deliberate: no component can quietly depend on data another team's fragment happens to fetch, so the byline team can remove a field without breaking the page. It also narrows re-renders to the components whose own selection changed. If the parent needs a field, it selects it itself; `@relay(mask: false)` disables masking on a spread, but the docs recommend against it.
code
tsx · 18 linesimport { graphql, useFragment } from 'react-relay';
import type { ArticleByline_article$key } from './__generated__/ArticleByline_article.graphql';
export function ArticleByline({ article }: { article: ArticleByline_article$key }) {
const data = useFragment(
graphql`
fragment ArticleByline_article on Article {
author { name avatarUrl }
}
`,
article,
);
return <p>By {data.author?.name}</p>;
}
// In ArticlePage, the query selects `title ...ArticleByline_article`.
// data.article.title is readable; data.article.author is not in its type,
// and at runtime it is undefined. The page passes data.article down as the reference.go deeper
Recall that useFragment returns only its own fragment's fields and that a parent passes the child a fragment reference, not the child's data.
Explain the mechanics: one composed query, a normalized store, opaque references carrying a record id, generated $key types, and why the parent sees undefined for a child's field.
Show the production payoff: safe field removal across teams, re-renders limited to the components whose selection changed, and when @inline with readInlineData is the right escape hatch.
Weigh masking as an organisational tool: it turns data dependencies into compiler-checked contracts between teams, at the price of some duplicated selections and a stricter mental model.
## The problem masking solves On a large news site the article page is assembled by several teams: one owns the headline, one the author byline, one the comment thread, one the like button. Each component declares its data needs as a **fragment** and the page query spreads them, so the whole page loads in **one request**. Without further rules, any component could read any field in that response. The headline component might start using `author.name` because the byline happened to fetch it; the day the byline team removes that field, the headline breaks, and nothing in the headline's own code changed. Relay calls these **implicit dependencies** and removes them with **data masking**: a component can read only the data its own fragment asked for. ## How a fragment reference works 1. The page's query spreads the child fragments: `article(id: $id) { title ...ArticleByline_article ...CommentThread_article }`. 2. The server returns one response. Relay **normalizes** it into the store: records keyed by `id`, fields stored once, whichever fragment asked for them. 3. The page calls `usePreloadedQuery` (or `useLazyLoadQuery`) and gets `data.article` containing `title` plus **fragment references**. A fragment reference is an opaque object that says *which record* to read and *which fragment* it carries; internally it holds the record id and fragment bookkeeping, but no field values. 4. The page renders `<ArticleByline article={data.article} />`. The byline calls `useFragment(ArticleBylineFragment, props.article)` and gets exactly the fields its fragment selected. The generated types enforce the same thing. The child's prop is typed `ArticleByline_article$key`, and the parent's `data.article` type contains a `$fragmentSpreads` marker for it, not the byline's fields. ## What each component can and cannot see | Reader | Sees | Does not see | |---|---|---| | Article page (query) | `title`, fragment references | the byline's `author { name }` | | `ArticleByline` (`useFragment`) | `author { name avatarUrl }` | `title`, unless it selects it too | | `CommentThread` (`useFragment`) | its own comment fields | the byline's fields | If two fragments select the same field, the store still keeps **one copy**; masking is a rule about reading, not a second cache. ## Consequences in a multi-team codebase - **Local reasoning.** A team can read its component and its fragment and know exactly what data it uses. - **Safe deletion.** Removing a field from your fragment cannot break another component's read, because nobody else could read it through you. Tooling helps: the `relay/unused-fields` lint rule flags selected fields a component never uses. - **Narrower re-renders.** Each `useFragment` subscribes to its own selection, so when a mutation changes the author's avatar, the byline re-renders and the page component does not. - **Missing spreads are caught.** If the page renders the byline but forgets to spread its fragment, the prop type does not match, and Relay warns that the byline's data is missing even if another fragment happened to fetch the same fields. ## How masking shows up in TypeScript The compiler turns masking into types, so most mistakes surface in the editor before any test runs: - The page's `data.article` type lists `title` and a `$fragmentSpreads` marker naming `ArticleByline_article`; there is no `author` property to autocomplete. - `ArticleByline` declares its prop as `ArticleByline_article$key`. Passing an object that lacks the byline spread is a type error, which is how a forgotten spread is caught. - `useFragment` infers its return type from the key, so the byline gets `author { name avatarUrl }` typed exactly, including nullability from the schema. The runtime enforces the same boundary for untyped code, so masking is not only a type-level convention. ## Escape hatches 1. **Select the field yourself.** If the page genuinely needs the author name, it adds `author { name }` to its own selection. Duplicated selections cost nothing extra on the wire or in the store. 2. **`@inline` with `readInlineData`.** For code outside React render, such as a formatter or an analytics helper, mark a fragment `@inline` and read it with `readInlineData`; the callers still spread the fragment, so the dependency stays declared. 3. **`@relay(mask: false)`.** Applied to a spread, it gives the parent the fragment's fields directly. The documentation recommends `@inline` instead and calls sharing one unmasked fragment across many components an anti-pattern that tends to over-fetch. ## Common mistakes - Thinking masking splits the request. It does not: all spread fragments compose into one query and one round trip. - Expecting `useFragment` to fetch. It reads from the store; data arrives through the query the fragment is spread into, and a fragment component can suspend only while that parent query is still in flight. - Passing the child's fields as props instead of the fragment reference, which throws away the type guarantee and the re-render isolation.
- A byline-formatting helper that runs outside React render needs the author fields; how do you give it data without breaking masking?Declare an `@inline` fragment for the helper and read it with `readInlineData(fragment, ref)`. Every component that calls the helper spreads that fragment, so the dependency is still declared and compiled, but the read happens outside a hook. The docs recommend this over `@relay(mask: false)`.
- Does masking mean the byline's fields are fetched in a separate request?No. All spread fragments are composed into the page's query at compile time, so there is one request and one normalized write to the store. Masking only controls which fields each `useFragment` call returns when reading that stored data.
A fragment reference is like a sealed envelope addressed to one colleague: the page can carry it and hand it over, but only the named recipient can open it with useFragment, so nobody else can come to rely on what is inside.
saying these in an interview costs you the question
- Masking hides the child's fields from the request, so the child fetches them separately.
- A parent can read a child's fields as long as it renders that child.
- useFragment sends its own network request when the component mounts.
- Marking spreads @relay(mask: false) is the recommended way to share data between components.
- Two fragments selecting the same field keep two copies of it in the store.