skip to content

Relay

Relay trades flexibility for guarantees: queries are compiled ahead of time, fragments are colocated and mandatory, and the store is normalized. Interviewers ask why a very large codebase might happily accept that strictness.

on this pageshow

questions

6

What does the relay-compiler do with the graphql-tagged queries and fragments in a Relay app, and why must it run before the app works?

level: juniorimportance: must knowfreq 38%

answer

  1. ahead of time, not at runtime
  2. checked against the schema file
  3. one artifact per operation or fragment
  4. the Babel plugin swaps the tag

basics

~20 s

The relay-compiler validates every graphql tag against the schema ahead of time and writes a generated artifact per query or fragment: runtime metadata, query text and types. A Babel plugin swaps each tag for its artifact, so uncompiled tags throw.

solid answer

~50 s

Relay does no GraphQL parsing in the browser. `relay-compiler`, configured by `relay.config.json` (`src`, `schema`, `language`), finds every `graphql` template literal, validates it against the schema file and fails the build on an unknown field, a bad argument or a naming violation: a fragment name must start with its module name, and an operation name must also end in `Query`, `Mutation` or `Subscription`. For each document it writes `__generated__/<Name>.graphql.ts` holding the metadata the runtime walks to read and normalize data, the query text sent to the server, and TypeScript types such as `$data`, `$key` and `$variables`. It also adds selections the runtime needs, like `id` and a connection's `cursor` and `pageInfo`, and generates the queries `@refetchable` asks for. The Babel plugin then replaces each tag with an import of its artifact; without it the tag throws "graphql: Unexpected invocation at runtime".

code

json · 5 lines
json
{
  "src": "./src",
  "schema": "./schema.graphql",
  "language": "typescript"
}

go deeper

for a junior

Recall the pipeline: the compiler validates graphql tags against the schema and writes artifacts, and the Babel plugin swaps each tag for its artifact. Know the naming rule and the generated folder.

for a middle

Explain what an artifact contains: reader and normalization metadata, the operation text and the $data, $key and $variables types, plus selections the compiler adds such as id and connection cursors.

for a senior

Show how you keep the pipeline honest across teams: --watch locally, --validate in CI, and why static documents and compile-time schema checks move failures from readers' browsers into builds.

for a principal

Frame the compiler as the price of Relay's guarantees: a mandatory build step and naming discipline, in exchange for schema-checked documents and generated types every team can rely on.

## Why Relay compiles GraphQL at all Many GraphQL clients accept a document at runtime, parse it in the browser and send it. **Relay moves that work to build time.** A command-line tool, `relay-compiler`, reads every `graphql` template literal in the source tree before the app is bundled, checks it, and writes **generated artifacts** that the runtime loads instead of the original text. On a news site where a dozen teams each own a slice of the article page, this means a broken selection fails in the build of the team that wrote it, not in a reader's browser. The compiler finds its settings in `relay.config.json`, in `relay.config.js`/`.mjs`/`.ts`, or under a `"relay"` key in `package.json`. A minimal config names three things: `src` (where to look for tags), `schema` (the SDL file to validate against) and `language` (`typescript` or `flow`). ## What the compiler checks Every document is validated before anything is written: - **Against the schema.** A misspelled field such as `article { heroImge }`, a missing required argument or a wrong variable type is a compile error, with the file and line. - **Naming.** Fragment names must start with the **module name**, derived from the file name: in `CommentThread.tsx` a fragment is `CommentThread_article`. Queries, mutations and subscriptions must also start with the module name and end in `Query`, `Mutation` or `Subscription`. This keeps names globally unique and traceable to a file. - **Static documents.** A tag may not contain `${...}` substitutions; the Babel plugin rejects them ("Substitutions are not allowed in graphql fragments"). Dynamic behaviour goes through variables, `@include`/`@skip` and fragment arguments. - **Conditional fragment spreads.** Since Relay 19, a spread under `@include`/`@skip`, or on a type that might not match, must carry `@alias`, so the parent reads the fragment reference as a nullable property. An unaliased one is a compile error. - **Directive rules**, for example that a `@connection` key ends with `_` plus the field name. ## What it writes By default each artifact lands in a `__generated__` folder next to the source file (an `artifactDirectory` setting can centralise them). For TypeScript the file is `<Name>.graphql.ts`. | Part of the artifact | Used for | |---|---| | Reader metadata | how `useFragment` and the query hooks read a selection out of the store | | Normalization metadata | how a response is split into records keyed by `id` | | Operation text (or a persisted id) | what the network layer sends | | Types: `$data`, `$key`, `$variables` | typing hook results, fragment-reference props and variables | | Generated operations | the pagination or refetch query that `@refetchable` requests | The compiler also **adds selections the runtime depends on**: it selects `id` on types that have one, so records can be merged, and adds `cursor`, `pageInfo.endCursor` and `hasNextPage` to a `@connection` field so pagination works without you writing them. ## How the artifacts reach the running app 1. `relay-compiler` runs (once, in `--watch` mode during development, or in the build) and writes the artifacts. 2. The bundler runs `babel-plugin-relay` (or a framework's equivalent, such as the SWC plugin) and replaces each `graphql` tag with an import of its artifact. Generated artifacts use ES module imports by default since Relay 19. 3. At runtime, hooks such as `useLazyLoadQuery` and `useFragment` receive the artifact, not text. If step 2 is missing, the `graphql` function that ships in `relay-runtime` executes for real, and it only throws: "graphql: Unexpected invocation at runtime. Either the Babel transform was not set up, or it failed to identify this call site." If step 1 is stale, the import points at an artifact that no longer matches the tag. ## Working with it day to day - Run `relay-compiler --watch` while editing, so artifacts follow the code. - In CI, run `relay-compiler --validate`: it writes nothing and exits non-zero if any artifact is out of date, which catches a teammate who forgot to recompile. - Import the generated types rather than hand-writing prop types: a child's prop is `CommentRow_comment$key`, and `useFragment` then returns the exactly typed `$data`. - On Relay 21 the packages ship their own TypeScript definitions; `@types/relay-runtime` is no longer needed (the definitions are present from 21.0.1). ## Common mistakes - Treating the generated folder as an optional cache. The app imports it; deleting it breaks the build. - Building a document string at runtime to "make it dynamic". Relay cannot compile what it cannot see; use variables or `@argumentDefinitions`. - Naming a fragment after the data instead of the file (`ArticleComments_article` in `CommentThread.tsx`), which the naming rule rejects.

  • Why does Relay reject a graphql tag that interpolates a string into the document?
    Relay compiles documents ahead of time, so every document the app can send must exist in the source as static text. The Babel plugin refuses substitutions outright. Variation belongs in GraphQL itself: operation variables, `@include`/`@skip`, and fragment arguments declared with `@argumentDefinitions` and passed with `@arguments`.
  • Since Relay 19, what does the compiler require of a fragment spread under @include or @skip?
    An `@alias` on the spread. The parent then reads the fragment reference as a named, nullable property, so code has to handle the case where the fragment was not fetched. An unaliased conditional spread is a compile error; `@dangerously_unaliased_fixme` exists only as a migration escape hatch.
  • How do you stop out-of-date Relay artifacts from being merged?
    Run `relay-compiler --validate` in CI. It computes what it would write, writes nothing, and exits non-zero if any artifact differs, so a change to a `graphql` tag without a recompile fails the pipeline instead of shipping a mismatched artifact.

saying these in an interview costs you the question

  • Relay parses the graphql template literal in the browser on first render.
  • A misspelled field only shows up as a null value at runtime.
  • The __generated__ files are an optional cache the app can run without.
  • Any globally unique fragment name compiles, whatever file it is in.
  • Relay 21 with TypeScript still needs @types/relay-runtime installed.
open as a page

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?

level: middleimportance: must knowfreq 36%

basics

~20 s

Relay'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.

open as a page

In Relay, how do useLazyLoadQuery and usePreloadedQuery with useQueryLoader differ in when an article page's query starts fetching?

level: middleimportance: should knowfreq 30%

basics

~20 s

useLazyLoadQuery starts fetching only when its component renders, so code loading and parent renders delay it and nested lazy queries waterfall. With useQueryLoader, an event handler or route transition calls loadQuery first, and usePreloadedQuery later reads that reference, suspending until it resolves.

open as a page

With Relay's usePaginationFragment, what must a news article's comment-thread fragment declare, and how does loadNext add the next page?

level: middleimportance: should knowfreq 27%

basics

~20 s

The fragment needs @argumentDefinitions for count and cursor, @refetchable(queryName) so the compiler generates a pagination query, and @connection(key) on the comments field. loadNext(n) sends that query from the current end cursor, and Relay appends the new edges to the stored connection.

open as a page

In Relay, a news article's like button and add-comment form both use useMutation; how do the like count and the comment thread update, instantly and after the server replies?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Relay merges payload objects into stored records by id, so a like payload selecting likeCount updates every reader; an optimisticResponse shows it instantly and is rolled back on reply. A new comment is inserted with @prependEdge and the thread's connection id.

open as a page

For a large news app whose article page is built by many teams, when is Relay's compiler-enforced strictness worth choosing over a more flexible GraphQL client?

level: principalimportance: should knowfreq 20%

basics

~20 s

Relay pays off when many teams share one screen: mandatory masking removes hidden data dependencies, fragments compose into one request per view, and the compiler rejects invalid documents. It costs a build step, Suspense-first loading and Node and connection schema conventions.

open as a page