Relay
Relay is the GraphQL client Facebook built and uses internally. Compared to Apollo / urql, it is more opinionated: queries are colocated with components, fragments compose by data needs, the Relay compiler verifies everything at build time, and the cache uses a strict global object identity (Node id). The payoff: massive React apps stay correct.
Schema conventions, fragments, useFragment, mutations
EXAMPLE
// 1) Schema conventions Relay relies on
//
// type Query { node(id: ID!): Node }
// interface Node { id: ID! }
//
// Every type Relay can fetch implements Node and has an opaque global id.
// Relay refetches a record by querying { node(id: $id) { ... on Order { ... } } }.
// 2) Pagination uses cursor-based Connections (the Relay spec)
//
// type Query {
// orders(first: Int, after: String, status: OrderStatus): OrderConnection!
// }
// type OrderConnection {
// edges: [OrderEdge!]!
// pageInfo: PageInfo!
// }
// type OrderEdge { cursor: String! node: Order! }
// type PageInfo { endCursor: String hasNextPage: Boolean! }
// 3) Setup
// npm i react-relay relay-runtime
// npm i -D relay-compiler @types/react-relay
// relay.config.js: { src: 'src', schema: 'schema.graphql', language: 'typescript' }
// Run compiler: npx relay-compiler
// Or via a Vite/Next plugin that runs it on save.
// 4) Environment
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
const network = Network.create(async (operation, variables) => {
const res = await fetch('/api/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: operation.text, variables }),
});
return res.json();
});
export const RelayEnvironment = new Environment({
network,
store: new Store(new RecordSource()),
});
// 5) Wrap your app
// import { RelayEnvironmentProvider } from 'react-relay';
// <RelayEnvironmentProvider environment={RelayEnvironment}>...</RelayEnvironmentProvider>
// 6) Co-located fragments — components declare WHAT they read
import { graphql } from 'react-relay';
import { useFragment } from 'react-relay';
const OrderRow_fragment = graphql\`
fragment OrderRow_order on Order {
id
customer
totalCents
status
}
\`;
export function OrderRow({ data }: { data: any }) {
const order = useFragment(OrderRow_fragment, data);
return (
<li>
{order.customer} — ${(order.totalCents / 100).toFixed(2)} — {order.status}
</li>
);
}
// 7) Parent component fetches what its children need via fragment composition
import { useLazyLoadQuery } from 'react-relay';
const OrdersList_query = graphql\`
query OrdersListQuery($first: Int!, $status: OrderStatus) {
orders(first: $first, status: $status) {
edges {
node {
id
...OrderRow_order # fragment spread
}
}
}
}
\`;
export function OrdersList() {
const data = useLazyLoadQuery<any>(OrdersList_query, { first: 20, status: 'OPEN' });
return (
<ul>
{data.orders.edges.map((e: any) => (
<OrderRow key={e.node.id} data={e.node} />
))}
</ul>
);
}
// 8) Pagination via usePaginationFragment — connection-aware
import { graphql, usePaginationFragment } from 'react-relay';
const OrdersPaginated_fragment = graphql\`
fragment OrdersPaginated_query on Query
@refetchable(queryName: 'OrdersPaginatedRefetch')
@argumentDefinitions(first: { type: 'Int!' }, after: { type: 'String' }) {
orders(first: $first, after: $after) @connection(key: 'Orders_orders') {
edges { node { id ...OrderRow_order } }
}
}
\`;
// 9) Mutations — the cache update is part of the contract
import { useMutation } from 'react-relay';
const cancelOrderMutation = graphql\`
mutation cancelOrderMutation($id: ID!) {
cancelOrder(id: $id) {
order { id status }
}
}
\`;
function CancelButton({ id }: { id: string }) {
const [commit, isInFlight] = useMutation(cancelOrderMutation);
return (
<button disabled={isInFlight} onClick={() => commit({ variables: { id } })}>
Cancel
</button>
);
}
// 10) Why Relay over Apollo / urql
// - Fragment colocation forces a clean component-data contract
// - The compiler verifies queries at BUILD time (no runtime gql parsing)
// - Connection spec gives consistent pagination across the codebase
// - Cache uses Node id => no per-mutation cache plumbing
//
// Why NOT
// - Bigger learning curve
// - Compiler step requires GraphQL discipline (Node id, connections)
// - Smaller third-party ecosystem
// - Smaller libraries (graphql-request, urql) ship faster
// 11) Pitfalls
// - Server schema without Node ids / connections -> Relay cannot help
// - Forgetting to run the compiler after editing a fragment
// - Mixing 'first/after' with 'limit/offset' across queries
// - Treating the cache as a manual store (let Relay's identity do the work)
Why it matters
Relay is the right choice when you have a big GraphQL app and the discipline to run the compiler. Fragment colocation + compiler validation make data dependencies explicit and refactorable in a way that ad-hoc clients cannot match — at the cost of more upfront setup and a strictly-spec server.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Relay — Meta's GraphQL client, cursor-based, schema-driven. // Best when you control both ends.Try it Yourself »
Discussion
Loading…