Apollo brain vs Relay brain
Why Relay Rejects Common GraphQL Patterns” Overfetching Manual cache writes Arbitrary queries everywhere
Apollo Brain vs Relay Brain
Understand the fundamental mental shift required when transitioning from Apollo Client to Relay, with free flashcards to reinforce these contrasting approaches. This lesson covers query-centric thinking, fragment-driven architecture, and colocation principles—essential concepts for mastering Relay's component-first philosophy. Moving from Apollo to Relay isn't just learning new syntax; it's rewiring how you think about data fetching in React applications.
Welcome to a Different Way of Thinking 🧠
If you're coming from Apollo Client, Relay will feel strange at first. Not because it's harder—it's actually more elegant once you understand it—but because it requires you to unlearn some deeply ingrained habits. Apollo encourages you to think in queries: "What data does this page need?" Relay forces a better question: "What data does this component need?"
This lesson will help you make that mental transition by contrasting the two approaches directly. By the end, you'll understand why Relay's fragment-based thinking leads to more maintainable, scalable applications.
The Fundamental Difference 🎯
Apollo Brain: Top-Down Query Thinking
When you work with Apollo, you typically:
- Start at the page level - Look at the entire route/page
- Write a big query - Fetch everything needed by all components
- Pass props down - Drill data through component trees
- Hope for the best - Trust components won't change their data needs
💭 Apollo Mental Model:
┌─────────────────────────────────────┐
│ PAGE COMPONENT │
│ useQuery(BIG_QUERY) { │
│ user { id, name, email } │
│ posts { id, title, likes } │
│ comments { id, text, author } │
│ } │
└──────────────┬──────────────────────┘
│ props flow down
↓
┌──────────┴──────────┐
↓ ↓
┌─────────┐ ┌─────────┐
│UserCard │ │PostList │
│ (props) │ │ (props) │
└─────────┘ └────┬────┘
│ props
↓
┌─────────┐
│PostItem │
│ (props) │
└─────────┘
Relay Brain: Bottom-Up Fragment Thinking
Relay inverts this completely:
- Start at the component - Each component declares its own needs
- Write fragments - Small, focused data requirements
- Colocate data with UI - Fragment lives next to component code
- Compose upward - Parent spreads child fragments
💭 Relay Mental Model:
┌─────────────────────────────────────┐
│ PAGE COMPONENT │
│ useQuery(ROOT_QUERY) { │
│ ...UserCard_fragment │
│ ...PostList_fragment │
│ } │
└──────────────┬──────────────────────┘
│ fragment refs
↓
┌──────────┴──────────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│ UserCard │ │ PostList │
│ │ │ │
│ fragment on │ │ fragment on │
│ User { │ │ Post { │
│ id, name │ │ ...PostItem│
│ } │ │ } │
└──────────────┘ └──────┬───────┘
│
↓
┌──────────────┐
│ PostItem │
│ │
│ fragment on │
│ Post { │
│ id, title │
│ } │
└──────────────┘
💡 Key Insight: In Relay, data requirements flow up (via fragment composition), while data itself flows down (via fragment refs). The parent never needs to know what data children need—only that they need it.
Apollo Brain Example: The Old Way 🏚️
Let's see how you'd build a user profile page in Apollo:
// Apollo approach - UserProfilePage.jsx
import { useQuery, gql } from '@apollo/client';
import UserHeader from './UserHeader';
import UserPosts from './UserPosts';
import UserFriends from './UserFriends';
const USER_PROFILE_QUERY = gql`
query UserProfileQuery($userId: ID!) {
user(id: $userId) {
# Data for UserHeader
id
name
email
avatar
bio
# Data for UserPosts
posts {
id
title
excerpt
publishedAt
likesCount
}
# Data for UserFriends
friends {
id
name
avatar
mutualFriendsCount
}
}
}
`;
function UserProfilePage({ userId }) {
const { data, loading } = useQuery(USER_PROFILE_QUERY, {
variables: { userId }
});
if (loading) return <Spinner />;
return (
<div>
<UserHeader user={data.user} />
<UserPosts posts={data.user.posts} />
<UserFriends friends={data.user.friends} />
</div>
);
}
🚨 Problems with Apollo Brain:
- The page knows too much - It needs to understand every child component's data requirements
- Fragile to changes - If
UserHeaderneeds a new field, you must modify the page query - No local reasoning - You can't understand
UserHeaderwithout checking the page query - Duplicate queries - If
UserHeaderis used elsewhere, you must remember to include those fields again - Props drilling - Deep components require passing data through intermediaries
Relay Brain Example: The New Way 🏗️
Here's the same feature in Relay:
// Relay approach - UserHeader.jsx
import { graphql, useFragment } from 'react-relay';
function UserHeader({ userRef }) {
const data = useFragment(
graphql`
fragment UserHeader_user on User {
id
name
email
avatar
bio
}
`,
userRef
);
return (
<header>
<img src={data.avatar} alt={data.name} />
<h1>{data.name}</h1>
<p>{data.bio}</p>
</header>
);
}
export default UserHeader;
// UserPosts.jsx
import { graphql, useFragment } from 'react-relay';
import PostItem from './PostItem';
function UserPosts({ postsRef }) {
const data = useFragment(
graphql`
fragment UserPosts_posts on Post @relay(plural: true) {
id
...PostItem_post
}
`,
postsRef
);
return (
<div>
{data.map(post => <PostItem key={post.id} postRef={post} />)}
</div>
);
}
// PostItem.jsx
import { graphql, useFragment } from 'react-relay';
function PostItem({ postRef }) {
const data = useFragment(
graphql`
fragment PostItem_post on Post {
id
title
excerpt
publishedAt
likesCount
}
`,
postRef
);
return (
<article>
<h3>{data.title}</h3>
<p>{data.excerpt}</p>
<time>{data.publishedAt}</time>
<span>{data.likesCount} likes</span>
</article>
);
}
// UserProfilePage.jsx - The page just composes!
import { graphql, useLazyLoadQuery } from 'react-relay';
import UserHeader from './UserHeader';
import UserPosts from './UserPosts';
import UserFriends from './UserFriends';
function UserProfilePage({ userId }) {
const data = useLazyLoadQuery(
graphql`
query UserProfilePageQuery($userId: ID!) {
user(id: $userId) {
...UserHeader_user
posts {
...UserPosts_posts
}
friends {
...UserFriends_friends
}
}
}
`,
{ userId }
);
return (
<div>
<UserHeader userRef={data.user} />
<UserPosts postsRef={data.user.posts} />
<UserFriends friendsRef={data.user.friends} />
</div>
);
}
✨ Benefits of Relay Brain:
- Local reasoning - Each component is self-contained
- Easy refactoring - Move components freely, their data follows
- Type safety - Generated types per fragment
- No duplication - Fragment names prevent field conflicts
- Gradual changes - Modify component data needs without touching pages
The Mental Shift: Seven Key Principles 🔄
1. Think Component-First, Not Page-First
❌ Apollo Brain: "This page needs user data, post data, and comments."
✅ Relay Brain: "This UserCard component needs id and name. Whatever page uses it will include those fields."
2. Fragments Are Your Primary Tool
❌ Apollo Brain: Fragments are optional convenience features.
✅ Relay Brain: Fragments are mandatory. Every component that needs data has a fragment. Queries just compose fragments.
3. Data Requirements Live Next to Components
❌ Apollo Brain: Queries live in page files or separate query files.
✅ Relay Brain: Fragment definitions are literally in the same file as the component. If you delete the component, its data requirements disappear too.
| Apollo Structure | Relay Structure |
|---|---|
src/
├── components/
│ ├── UserCard.jsx
│ └── PostList.jsx
├── queries/
│ └── userQueries.js
└── pages/
└── UserPage.jsx
|
src/
├── components/
│ ├── UserCard.jsx
│ │ (fragment inside)
│ └── PostList.jsx
│ (fragment inside)
└── pages/
└── UserPage.jsx
(query composes fragments)
|
4. Parents Don't Know Child Data Needs
❌ Apollo Brain: Parent queries must list every field every child needs.
✅ Relay Brain: Parents just spread child fragments. The Relay compiler handles the rest.
// Apollo: Parent must know details
const PARENT_QUERY = gql`
query {
user {
id # for UserCard
name # for UserCard
email # for UserCard
posts { # for PostList
id # for PostList
title # for PostList
}
}
}
`;
// Relay: Parent just spreads
const PARENT_QUERY = graphql`
query {
user {
...UserCard_user
posts {
...PostList_posts
}
}
}
`;
5. Components Receive "References," Not Data
❌ Apollo Brain: Components receive plain JavaScript objects.
✅ Relay Brain: Components receive opaque "fragment references" that only useFragment can read.
// Apollo: Direct data access
function UserCard({ user }) {
return <h1>{user.name}</h1>; // user is a plain object
}
// Relay: Must use useFragment
function UserCard({ userRef }) {
const user = useFragment(fragment, userRef); // userRef is opaque
return <h1>{user.name}</h1>;
}
Why? This prevents components from accessing data they didn't declare. It enforces fragment discipline.
6. Compiler Does the Heavy Lifting
❌ Apollo Brain: You manually ensure queries match component needs.
✅ Relay Brain: The Relay compiler validates everything at build time. If a fragment doesn't exist, compilation fails. If you spread a fragment on the wrong type, compilation fails.
7. Composition Is Explicit and Typed
❌ Apollo Brain: Runtime prop-drilling, hope types match.
✅ Relay Brain: Generated TypeScript/Flow types ensure parent queries satisfy child fragments.
A Real-World Migration Story 🔄
Let's watch someone transition from Apollo Brain to Relay Brain on a real component.
Before: Apollo Brain (Problem)
// DashboardPage.jsx - Apollo version
import { useQuery, gql } from '@apollo/client';
import StatsCard from './StatsCard';
import ActivityFeed from './ActivityFeed';
import TeamList from './TeamList';
const DASHBOARD_QUERY = gql`
query DashboardQuery {
currentUser {
# For StatsCard
postsCount
followersCount
likesReceived
# For ActivityFeed
recentActivity {
id
type
description
timestamp
actor {
id
name
avatar
}
}
# For TeamList
teams {
id
name
memberCount
isAdmin
}
}
}
`;
function DashboardPage() {
const { data, loading } = useQuery(DASHBOARD_QUERY);
if (loading) return <Spinner />;
return (
<div>
<StatsCard stats={data.currentUser} />
<ActivityFeed activities={data.currentUser.recentActivity} />
<TeamList teams={data.currentUser.teams} />
</div>
);
}
😫 Pain Points:
- Someone adds a new field to
ActivityFeed→ must editDashboardPage.jsx - Want to reuse
TeamListelsewhere → must remember to includeid, name, memberCount, isAdmin - Change
StatsCardto usecommentsCountinstead oflikesReceived→ must update dashboard query
After: Relay Brain (Solution)
// StatsCard.jsx
import { graphql, useFragment } from 'react-relay';
function StatsCard({ userRef }) {
const data = useFragment(
graphql`
fragment StatsCard_user on User {
postsCount
followersCount
likesReceived
}
`,
userRef
);
return (
<div className="stats">
<div>{data.postsCount} posts</div>
<div>{data.followersCount} followers</div>
<div>{data.likesReceived} likes</div>
</div>
);
}
export default StatsCard;
// ActivityFeed.jsx
import { graphql, useFragment } from 'react-relay';
function ActivityFeed({ activitiesRef }) {
const activities = useFragment(
graphql`
fragment ActivityFeed_activities on Activity @relay(plural: true) {
id
type
description
timestamp
actor {
id
name
avatar
}
}
`,
activitiesRef
);
return (
<ul>
{activities.map(activity => (
<li key={activity.id}>
<img src={activity.actor.avatar} alt={activity.actor.name} />
<span>{activity.description}</span>
<time>{activity.timestamp}</time>
</li>
))}
</ul>
);
}
export default ActivityFeed;
// DashboardPage.jsx - Relay version
import { graphql, useLazyLoadQuery } from 'react-relay';
import StatsCard from './StatsCard';
import ActivityFeed from './ActivityFeed';
import TeamList from './TeamList';
function DashboardPage() {
const data = useLazyLoadQuery(
graphql`
query DashboardPageQuery {
currentUser {
...StatsCard_user
recentActivity {
...ActivityFeed_activities
}
teams {
...TeamList_teams
}
}
}
`,
{}
);
return (
<div>
<StatsCard userRef={data.currentUser} />
<ActivityFeed activitiesRef={data.currentUser.recentActivity} />
<TeamList teamsRef={data.currentUser.teams} />
</div>
);
}
export default DashboardPage;
🎉 Benefits Realized:
ActivityFeedneeds a new field → just editActivityFeed.jsx, run compiler, done- Reuse
TeamList→ just spread...TeamList_teamsin any query - Change
StatsCard→ edit its fragment, no other files touched - Type safety → TypeScript knows exactly what shape
userRefhas
Common Mistakes When Switching 🚨
Mistake 1: Trying to Access Data Without useFragment
// ❌ WRONG: Apollo Brain trying Relay
function UserCard({ userRef }) {
// This won't work! userRef is opaque
return <h1>{userRef.name}</h1>;
}
// ✅ CORRECT: Relay Brain
function UserCard({ userRef }) {
const user = useFragment(
graphql`fragment UserCard_user on User { name }`,
userRef
);
return <h1>{user.name}</h1>;
}
Why it fails: Fragment references are intentionally opaque. This enforces declaring your data needs.
Mistake 2: Writing Queries Instead of Fragments
// ❌ WRONG: Apollo Brain in component
import { useLazyLoadQuery } from 'react-relay';
function UserCard({ userId }) {
// Don't do this in a reusable component!
const data = useLazyLoadQuery(
graphql`query UserCardQuery($id: ID!) {
user(id: $id) { name, avatar }
}`,
{ id: userId }
);
return <div>{data.user.name}</div>;
}
// ✅ CORRECT: Relay Brain
import { useFragment } from 'react-relay';
function UserCard({ userRef }) {
const data = useFragment(
graphql`fragment UserCard_user on User { name, avatar }`,
userRef
);
return <div>{data.name}</div>;
}
Why it matters: Queries create data fetching boundaries. Fragments participate in data fetching initiated elsewhere. Components should use fragments; only routes/pages should use queries.
Mistake 3: Prop Drilling Plain Data
// ❌ WRONG: Apollo Brain pattern
function ParentComponent({ userRef }) {
const user = useFragment(fragment, userRef);
return <ChildComponent user={user} />; // Passing plain data!
}
function ChildComponent({ user }) {
return <div>{user.name}</div>; // Child doesn't declare needs!
}
// ✅ CORRECT: Relay Brain pattern
function ParentComponent({ userRef }) {
return <ChildComponent userRef={userRef} />; // Pass the ref!
}
function ChildComponent({ userRef }) {
const user = useFragment(
graphql`fragment ChildComponent_user on User { name }`,
userRef
);
return <div>{user.name}</div>;
}
Why it matters: When you read the fragment and pass plain data, children can't declare their own needs. Always pass fragment refs down.
Mistake 4: Not Trusting the Compiler
// ❌ WRONG: Apollo Brain safety checks
function UserCard({ userRef }) {
if (!userRef) return null; // Unnecessary!
if (!userRef.name) return null; // Unnecessary!
const user = useFragment(fragment, userRef);
return <h1>{user.name}</h1>;
}
// ✅ CORRECT: Relay Brain trust
function UserCard({ userRef }) {
const user = useFragment(
graphql`fragment UserCard_user on User { name }`,
userRef
);
// If name is required in the fragment, it's guaranteed to exist
return <h1>{user.name}</h1>;
}
Why it matters: Relay's compiler validates the entire data flow. If your fragment declares a field, Relay guarantees it exists (unless nullable in schema).
Mistake 5: Creating Mega-Fragments
// ❌ WRONG: Apollo Brain mega-query as fragment
const USER_FRAGMENT = graphql`
fragment MegaFragment_user on User {
id
name
email
avatar
bio
postsCount
followersCount
posts {
id
title
body
comments { id, text }
}
friends {
id
name
avatar
}
}
`;
// ✅ CORRECT: Relay Brain granular fragments
const USER_HEADER_FRAGMENT = graphql`
fragment UserHeader_user on User {
name
avatar
bio
}
`;
const USER_STATS_FRAGMENT = graphql`
fragment UserStats_user on User {
postsCount
followersCount
}
`;
// Compose them where needed
const PAGE_QUERY = graphql`
query UserPageQuery($id: ID!) {
user(id: $id) {
...UserHeader_user
...UserStats_user
posts {
...PostList_posts
}
}
}
`;
Why it matters: Small, focused fragments are reusable and maintainable. Each component should only declare what it directly renders.
Key Takeaways 🎓
📋 Apollo Brain → Relay Brain Cheat Sheet
| Concept | Apollo Brain ❌ | Relay Brain ✅ |
|---|---|---|
| Primary Unit | Query (at page level) | Fragment (at component level) |
| Data Flow | Top-down (page → children) | Requirements flow up, data flows down |
| Colocation | Queries in separate files | Fragments in component files |
| Parent Knowledge | Parent knows all child fields | Parent spreads child fragments |
| Props | Pass plain data objects | Pass fragment references |
| Type Safety | Manual type annotations | Auto-generated per fragment |
| Refactoring | Update multiple files | Update single component file |
| Validation | Runtime errors | Compile-time errors |
| Reusability | Must remember field lists | Just spread the fragment |
| Mental Model | "What does this page need?" | "What does this component need?" |
The Transition Process 🛤️
When learning Relay, expect to go through these stages:
Stage 1: Frustration (Week 1) 😤
- "Why can't I just access the data?"
- "This seems like so much boilerplate!"
- "Apollo was simpler!"
Stage 2: Understanding (Week 2-3) 💡
- "Oh, fragment refs enforce data requirements..."
- "The compiler catches mistakes I'd miss in Apollo"
- "Components are actually more portable now"
Stage 3: Appreciation (Week 4+) 🎉
- "I refactored this without touching the page!"
- "New component? Just write its fragment, done"
- "I can't go back to Apollo's prop drilling"
Stage 4: Mastery (Month 2+) 🏆
- Thinking in fragments becomes natural
- Designing component APIs around fragment composition
- Teaching others why Relay is worth the learning curve
💭 Did You Know?
Relay was built at Facebook to solve problems they encountered with their first GraphQL client (which was similar to Apollo). After years of prop drilling bugs and query duplication issues across thousands of components, they realized queries and components needed to be inverted: components should declare needs, not pages. This led to the fragment-first architecture.
The name "Relay" itself refers to how data requirements are "relayed" from components up to the query, then data is relayed back down.
📚 Further Study
- Relay Documentation - Thinking in Relay: https://relay.dev/docs/principles-and-architecture/thinking-in-relay/
- Relay Compiler Architecture: https://relay.dev/docs/guides/compiler/
- Apollo to Relay Migration Guide: https://relay.dev/docs/migration/apollo-to-relay/
🔑 Remember: You're not learning new syntax—you're learning a fundamentally different way to architect data fetching. Apollo asks "what data do I need?"; Relay asks "what data does this component need?" That shift is everything.