10

The features people don't know Relay has

Incremental delivery, server-side preloading, and field-level error handling. The first four sections run live against this app's server. It speaks @defer/@stream through graphql-js 17, streaming newline-delimited JSON that the Relay network layer feeds to the executor payload by payload.

@defer: paint the fast part now, stream the slow part in

Mark a fragment spread with @defer and the server sends it as a later payload of the same response. The component reading it suspends on its own, so only that part of the UI shows a placeholder. No second query, no waterfall, no loading flags.

Fetching with Relay…

@stream: render list items as the server produces them

@stream(initialCount: 1) sends the first item with the initial payload and every later item as soon as its resolver yields. Relay appends each one to the same list record. For connections there's @stream_connection.

Fetching with Relay…

Server components: preload on the server, hydrate the store

This page is a React Server Component. It executed ServerPreloadedQuery in-process while rendering, then handed the raw payload to a client component that commits it to the Relay store before reading. The data is in the first HTML byte, and the client never refetches.

Executed on the server
4 ms
Client requests for this query
0
Rendered at
01:25:55
  • Why colocated fragments scale
  • The normalized cache, explained
  • Optimistic UI without the tears
  • Cursor pagination in practice

This list was in the HTML before any JavaScript ran. The like buttons still work: the server's payload was written into the client store, so mutations, the devtools drawer and every other page share it.

// app/advanced/page.tsx  (server component)
import Query from "@/__generated__/ServerPreloadedQuery.graphql";

export default async function Page() {
  const { response } = await executePersisted(Query);
  return <ServerPreloaded response={response} />;
}

// ServerPreloaded.tsx  ("use client")
const op = createOperationDescriptor(getRequest(query), {});
environment.commitPayload(op, response.data);   // once
environment.retain(op);                          // keep it from GC

useLazyLoadQuery(query, {}, { fetchPolicy: "store-only" });

@required & @catch: nullability you can reason about

GraphQL makes almost everything nullable so one failing field doesn't fail the whole response. Relay gives components two ways to say what they actually need, and the generated types follow.

Fetching with Relay…

More hidden gems

Not every feature needs a live demo to make the case. These are the ones teams discover late and wish they had known about earlier.

Relay Resolvers

Model derived and client-only state as fields in the schema. Components read them through fragments like any server field, and live resolvers can subscribe to an external store.

/**
 * @relayField User.initials: String
 * @rootFragment UserInitialsFragment
 */
export function initials(key) { … }
@module / @match (3D)

Data-driven dependencies: for a union or interface, the server picks which component renders each member, and Relay loads just that component's code with the data.

attachment {
  ...ImageAttachment_a @module(name: "ImageAttachment")
  ...VideoAttachment_a @module(name: "VideoAttachment")
}
EntryPoints

Declare a route's queries and its component together, then start loading both on hover or navigation, before React renders anything. Render-as-you-fetch at the route level.

const ref = loadEntryPoint(env, PostEntryPoint, { id });
<EntryPointContainer entryPointReference={ref} />
@alias (enforced in Relay 21)

A spread whose type might not match (a Post fragment on a Node field) must be aliased, so it becomes a nullable property instead of silently reading nothing. This app hit that compile error while being built.

node(id: $id) {
  ...PostCard_post @alias(as: "post")
}
// data.node.post: PostCard_post$key | null
@throwOnFieldError + @semanticNonNull

Opt a fragment into throwing field errors to the nearest error boundary. Fields the schema marks @semanticNonNull (null only on error) then get non-null types.

# schema
type User { name: String @semanticNonNull }

fragment Profile_user on User @throwOnFieldError {
  name   # string, not string | null
}
@inline + readInlineData

Read a fragment outside React (in a sort comparator, analytics call or utility) without subscribing or rendering, and keep the same masking and colocation.

const post = readInlineData(
  graphql`fragment rank_post on Post @inline { likeCount }`,
  ref,
);
Declarative store directives

Update connections straight from the mutation response. No updater function and no store-traversal code.

addComment(input: $input) {
  commentEdge @appendEdge(connections: $conns) { … }
}
deletePost(input: $i) { id @deleteRecord }
@updatable + typed local writes

When you do need to write to the store imperatively, @updatable fragments and queries give you a typed, assignable proxy instead of string-keyed record APIs.

const { updatableData } = store.readUpdatableQuery(q, {});
updatableData.viewer.name = "Ada";
Garbage collection & invalidation

Relay reference-counts every query. Records nothing retains are collected, and invalidateStore() or invalidateRecord() mark data stale so the next read refetches.

new Store(source, { gcReleaseBufferSize: 10 });
commitLocalUpdate(env, s => s.invalidateStore());