Part 3 · 2 chapters · ~20 min
State and Data Layers
Relay as the data layer for thousands of components on one graph (fragments, composition, the compiler, the normalised store, masking) with its costs, and the three ways a company shapes data for screens (RPC, GraphQL with a compiler, the BFF) as a question of who owns the shape.
7
Relay: fragments, masking and the compiler
code
// a Relay component: the fragment is the data contract; the compiler generates the type; the runtime masks
import { graphql, useFragment } from 'react-relay'
import type { PostCard_post$key } from './__generated__/PostCard_post.graphql' // generated: exactly the fragment's shape
export function PostCard({ post }: { post: PostCard_post$key }) { // the prop is an opaque reference, not the data
const data = useFragment(graphql`
fragment PostCard_post on Post {
id
title
author { name avatarUrl }
}
`, post)
// data.body is a type error AND undefined at runtime: the parent may have fetched it; this component did not declare it
return <article><img src={data.author.avatarUrl} alt="" /><h3>{data.title}</h3><span>{data.author.name}</span></article>
}
// the parent spreads the child's fragment; it never names the child's fields
export function Feed({ viewer }: { viewer: Feed_viewer$key }) {
const data = useFragment(graphql`
fragment Feed_viewer on Viewer {
posts(first: 20) @connection(key: "Feed_posts") { edges { node { id ...PostCard_post } } }
}
`, viewer)
return <>{data.posts.edges.map(e => <PostCard key={e.node.id} post={e.node} />)}</>
}
// the route query (compiled to a persisted id): query FeedRouteQuery { viewer { ...Feed_viewer } }
// a mutation that returns the updated Post updates every PostCard reading it, via the normalised store; @appendNode / @deleteRecord edit connectionsthe data layer for thousands of components on one graph
- The fragment: a component declares the fields it reads on the type it renders and receives an opaque reference; its data needs are colocated with its render. It cannot read a field it did not declare, even if the parent fetched it.
- Composition: a parent spreads its children's fragments; the route's query spreads the top fragment; the compiler assembles one deduplicated query per route from the tree. Adding a component adds its fields; removing it removes them; no team coordinates a query.
- The compiler: validates every fragment against the schema at build (a removed field is a build error, not a runtime undefined), generates types so a component's props are exactly its fragment's shape, and emits persisted query ids (an id over the wire; an allowlist on the server). The Architecture course part 2's contract, enforced by a compiler on every build.
- The normalised store: records by global id; a mutation that returns the updated record updates every reader (the FSD course M1 v3 option B, owned by the framework); connections with cursors are first-class with directives that tell the store how to append and delete.
- Masking at runtime is what makes "who reads this field" a fragment search and an unread field deletable from the schema: part 0's fossil of review discipline, as a guarantee.
- Costs and signal: GraphQL and a schema team, a compiler in the build, fragments on every component, store updaters for mutations, a steep curve. It pays when many teams read one graph, fields cannot be removed because nobody knows who reads them, and hand-written queries per screen drift. Apollo gives the store without compiler-enforced masking; urql is lighter still.
RELAY: FRAGMENTS, MASKING AND THE COMPILER
a component declares what it reads; the compiler assembles the query; the store hands each component only its slice
swipe the figure sideways, or tap expand for full screen
1/6
the fragment
The fragment: a component declares graphql`fragment PostCard_post on Post { id title author { name avatarUrl } }` and receives a post reference as a prop. It cannot read post.body: the field is not in its fragment, and the runtime masks it even if another component on the page fetched it. The component's data dependency is colocated with the component and visible in the file.
8
RPC, GraphQL and the BFF
who composes the data, and who owns the shape
- RPC (protobuf, gRPC-web or a JSON mapping): typed methods versioned by field numbers (add freely, never reuse, deprecate rather than remove), generated clients, the screen composing several calls or a BFF composing them. Optimises service ownership, stability and efficiency; over-fetching is per method.
- GraphQL with a compiler: one graph composed by the component tree and masked per component; a schema team owns the graph and resolvers delegate to services (N+1 solved with data loaders). Optimises portability, exact fetching, one round trip per route.
- The BFF: a server per surface owned by the frontend team returning exactly the screen's shape (the FSD course M7's hydrated feed item), with caching, auth and experiments server-side. Optimises the frontend team's autonomy; costs a service to run and logic that drifts between BFFs.
- Persisted and allowlisted, always: no large company lets the client send arbitrary queries in production. Persisted GraphQL ids, RPC methods as the allowlist, BFF endpoints as the contract: the server knows every shape before it ships, which is what makes capacity, caching and security tractable.
- Masking elsewhere: Apollo and urql cache and fragment without enforced masking by default; TanStack Query caches by key without normalising (M1 v3 option A); a lint forbidding reads outside the fragment is the compiler's job done worse.
- Choosing: a few teams on a few services → RPC and a BFF; many teams composing one product graph → GraphQL with a compiler; platform-specific screens → BFFs per platform over either. The question is who should own the shape of a screen's data; each answer is an org decision wearing a protocol.
the transferable part
Colocate a component's data needs with the component; generate types from the contract; never send arbitrary queries in production; normalise when many views share entities. Relay is these four as a compiler; the four hold without it.
RPC, GRAPHQL AND THE BFF
three ways a company shapes data for its screens, and what each one optimises
swipe the figure sideways, or tap expand for full screen
1/6
RPC
RPC: a service exposes typed methods (GetUser, ListPosts) with protobuf messages, versioned by field numbers (add fields freely; never reuse a number; deprecate, do not remove); the client is generated from the proto; the screen calls several methods and composes. Optimises: service ownership and stability, binary efficiency, cross-language clients. Costs: the screen composes (N calls, or a BFF), and over-fetching is per method, not per field.