You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Relay

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:

  1. Start at the page level - Look at the entire route/page
  2. Write a big query - Fetch everything needed by all components
  3. Pass props down - Drill data through component trees
  4. 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:

  1. Start at the component - Each component declares its own needs
  2. Write fragments - Small, focused data requirements
  3. Colocate data with UI - Fragment lives next to component code
  4. 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:

  1. The page knows too much - It needs to understand every child component's data requirements
  2. Fragile to changes - If UserHeader needs a new field, you must modify the page query
  3. No local reasoning - You can't understand UserHeader without checking the page query
  4. Duplicate queries - If UserHeader is used elsewhere, you must remember to include those fields again
  5. 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:

  1. Local reasoning - Each component is self-contained
  2. Easy refactoring - Move components freely, their data follows
  3. Type safety - Generated types per fragment
  4. No duplication - Fragment names prevent field conflicts
  5. 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 edit DashboardPage.jsx
  • Want to reuse TeamList elsewhere → must remember to include id, name, memberCount, isAdmin
  • Change StatsCard to use commentsCount instead of likesReceived → 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:

  • ActivityFeed needs a new field → just edit ActivityFeed.jsx, run compiler, done
  • Reuse TeamList → just spread ...TeamList_teams in any query
  • Change StatsCard → edit its fragment, no other files touched
  • Type safety → TypeScript knows exactly what shape userRef has

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


🔑 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.