Skip to content

powercodeacademy/phrg-apollo-basics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

15 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Apollo Client Code-Along: useQuery & useMutation

Welcome to your hands-on Apollo Client lesson! You'll be building a Pokemon favorites app while learning how to use Apollo Client's useQuery and useMutation hooks.

๐ŸŽฏ Learning Objectives

By the end of this code-along, you will be able to:

  • Explain why Apollo Client is useful for GraphQL applications
  • Write GraphQL queries and use them with the useQuery hook
  • Implement mutations with the useMutation hook
  • Handle loading states, errors, and cache updates
  • Build a complete React app that fetches and modifies data

๐Ÿ“‹ Prerequisites

  • Solid understanding of React hooks and TypeScript
  • Basic GraphQL knowledge (queries, mutations, variables)
  • Node.js 18+ installed

๐Ÿš€ Getting Started

  1. Fork and clone this repository
  2. Install dependencies:
    npm install
  3. Start the development server:
    npm run dev
  4. Verify everything is working:
    npm run verify
  5. Open http://localhost:5173 in your browser

You should see placeholder components that you'll be building step by step. Each component has the UI structure ready, but you'll add the Apollo Client functionality!

๐Ÿ“ Note: Starter files with placeholder content have been created for you in src/components/. You'll add Apollo Client imports, queries, and mutations to make them work.

๐Ÿ“š Code-Along Structure

We'll build this app step by step:

  1. Part 1: Understanding Apollo Client & Setup
  2. Part 2: Your First Query with useQuery
  3. Part 3: Creating Data with useMutation
  4. Part 4: Advanced Patterns & Best Practices
  5. Part 5: Optional Practice - Build Your Own Feature!

Part 1: Understanding Apollo Client ๐Ÿง 

๐Ÿ“– Required Reading

Before starting this section, read the official Apollo Client documentation:

What is Apollo Client?

Apollo Client is a comprehensive state management library that helps you manage GraphQL data in React applications. Think of it as a smart layer between your React components and your GraphQL API that handles:

  • Caching: Stores data so you don't re-fetch unnecessarily
  • Loading states: Tracks when requests are in progress
  • Error handling: Manages network and GraphQL errors
  • Cache updates: Keeps your UI in sync when data changes

Why Use Apollo Client?

Let's compare the traditional approach vs Apollo Client:

Without Apollo Client (lots of boilerplate!)

const [pokemon, setPokemon] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

useEffect(() => {
  setLoading(true)
  fetch("/graphql", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query: `query GetPokemon($name: String!) {
        pokemon(name: $name) { id name types }
      }`,
      variables: { name: "pikachu" },
    }),
  })
    .then((res) => res.json())
    .then((data) => setPokemon(data.pokemon))
    .catch((err) => setError(err))
    .finally(() => setLoading(false))
}, [])

With Apollo Client (clean and declarative!)

const { data, loading, error } = useQuery(GET_POKEMON, {
  variables: { name: "pikachu" },
})

Much cleaner, right? Apollo handles all the state management for you!

Explore the Demo App

Take a look at the app at http://localhost:5173:

You'll see two sections with placeholder content:

  1. Pokemon Search - Has a search input but shows "No Pokemon found - add your useQuery hook!"
    • Note: The search requires exact Pokemon names (lowercase, full names like "pikachu", "charizard")
  2. My Pokemon Favorites - Has a form but shows "No favorites yet - add your Apollo hooks to see data!"

Your job will be to make these components work by adding Apollo Client functionality!


Part 2: Building an Interactive Pokemon Search ๐Ÿ”

๐Ÿ“– Required Reading

Before starting this section, read through the "Executing a query" section in the official documentation about queries:

Now let's add Apollo Client functionality to create an interactive Pokemon search! Look at your app - you'll see a search input that shows "No Pokemon found - add your useQuery hook!" Let's make it work step by step.

Step 1: Understanding GraphQL Queries

First, let's look at the GraphQL query we'll be using. Open the GraphQL Playground and try this query:

query GetPokemon($pokemon: PokemonEnum!) {
  getPokemon(pokemon: $pokemon) {
    key
    species
    types {
      name
    }
    sprite
  }
}

Try it yourself:

  1. Go to https://graphqlpokemon.favware.tech/v8
  2. Paste the query above
  3. In the variables section (bottom left), add: {"pokemon": "pikachu"}
  4. Click the play button

You should see Pokemon data returned! This is the same API our app uses.

โš ๏ธ Important: This API requires exact Pokemon names (lowercase, full names only). Examples that work: "pikachu", "charizard", "bulbasaur". Partial names like "pika" won't work.

Step 2: Add Apollo Client Imports

Open src/components/PokemonSearch.tsx. You'll see it has the search input and UI structure but no Apollo functionality yet.

๐ŸŽฏ Your Task: Add the Apollo Client imports at the top of the file.

Replace the first TODO comment with:

import { gql, useQuery } from "@apollo/client"

๐Ÿง  What are these?

  • gql: A template literal tag that parses GraphQL query strings. It helps with syntax highlighting and validation.
  • useQuery: A React hook that executes GraphQL queries and manages loading, error, and data states for you.

๐Ÿ’พ Save and check: Your app should still show "No Pokemon found - add your useQuery hook!"

Step 3: Define Your GraphQL Query

๐ŸŽฏ Your Task: Add your GraphQL query after the imports.

Replace the "TODO: Add your GraphQL query here" comment with:

const GET_POKEMON = gql`
  query GetPokemon($pokemon: PokemonEnum!) {
    getPokemon(pokemon: $pokemon) {
      key
      species
      types {
        name
      }
      baseStats {
        hp
        attack
        defense
        speed
      }
      sprite
    }
  }
`

๐Ÿง  What's happening here?

  • gql parses our GraphQL query string and turns it into a format Apollo can use
  • $pokemon: PokemonEnum! defines a variable that we'll pass in (the ! means it's required)
  • We're asking for specific fields: key, species, types, and sprite
  • This is like writing a "shopping list" of data we want from the API

๐Ÿ’พ Save and check: Still should show the same placeholder - we haven't used the query yet!

Step 4: Add TypeScript Types

๐ŸŽฏ Your Task: Add the query types after the existing Pokemon type.

Add these types after the Pokemon type definition:

type GetPokemonData = {
  getPokemon: Pokemon | null
}

type GetPokemonVars = {
  pokemon: string
}

๐Ÿง  Why do we need these types?

  • GetPokemonData: Describes the shape of data coming back from our query. The API might return a Pokemon or null if not found.
  • GetPokemonVars: Describes the variables we're sending to the query. We need to pass a pokemon string.
  • TypeScript uses these to give us autocomplete and catch errors before we run the code!

๐Ÿ’พ Save and check: Still showing placeholder - that's expected!

Step 5: Add the useQuery Hook with Skip Option

๐ŸŽฏ Your Task: Replace the "TODO: Add your useQuery hook here (with skip option)" comment with:

const { data, loading, error } = useQuery<GetPokemonData, GetPokemonVars>(
  GET_POKEMON,
  {
    variables: { pokemon: searchTerm },
    skip: !searchTerm || searchTerm.length < 3,
  }
)

๐Ÿง  What's useQuery doing?

  • Automatically executes our GraphQL query when the component mounts
  • Manages three states for us:
    • loading: true while the request is in progress
    • error: Contains error info if something goes wrong
    • data: Contains the response data when successful
  • Handles variables: We pass pokemonName as the $pokemon variable
  • TypeScript generics: <GetPokemonData, GetPokemonVars> tells TypeScript what types to expect

โš ๏ธ TypeScript Note: You might see TypeScript errors about duplicate variable names (loading, error). This is expected! We have both the placeholder variables and the real ones from useQuery. Step 6 will fix this by removing the placeholders.

Step 6: Connect the Real Data

๐ŸŽฏ Step 6a: Remove the placeholder variables.

Delete these three lines:

const loading = false
const error = null
const pokemon: Pokemon | null = null

๐Ÿง  Why remove these? These were fake placeholder values. Now we want to use the real loading and error from useQuery!

๐ŸŽฏ Step 6b: Add the real pokemon variable.

Add this line after your useQuery hook:

const pokemon = data?.getPokemon

๐Ÿง  What's data?.getPokemon?

  • The ?. is called "optional chaining" - it safely accesses getPokemon even if data is undefined
  • This prevents crashes while the query is loading (when data is still undefined)
  • Once the query completes, data.getPokemon will contain our Pokemon object

๐Ÿ’พ Save and check: ๐ŸŽ‰ Now try typing "pikachu" in the search box - you should see real Pokemon data!

๐Ÿ” Search Tips: The Pokemon API requires exact Pokemon names. Try these examples:

  • "pikachu" โœ…
  • "charizard" โœ…
  • "bulbasaur" โœ…
  • "pika" โŒ (partial names won't work)
  • "Pikachu" โŒ (case sensitive - use lowercase)

๐Ÿง  What You Just Built: You've created a complete GraphQL-powered React component! Here's what Apollo Client is doing for you:

  1. Automatic Network Requests: useQuery sends HTTP requests to the GraphQL API
  2. State Management: No need for useState to track loading/error/data
  3. Caching: Try switching between Pokemon names - Apollo caches results for better performance
  4. Type Safety: TypeScript prevents bugs by checking data types
  5. Error Handling: Built-in error states for network issues or invalid data

Discussion Questions:

  • What happens during the loading state? (Watch for the brief "Loading..." message)
  • How does Apollo handle errors vs. "not found"? (Try an invalid Pokemon name)
  • Why is caching important for user experience?
  • What would this component look like if you built it with just fetch() and useState?

Part 3: Creating Data with useMutation ๐Ÿš€

๐Ÿ“– Required Reading

Before starting this section, read through the "Executing a mutation" section in the official documentation about mutations:

Now let's learn about mutations - GraphQL operations that modify data! You'll see how Apollo Client makes it easy to create new data and keep your UI in sync.

Understanding Mutations vs Queries

Before we dive in, let's understand the key differences:

Queries (what you learned in Part 2)

  • Read data from the server
  • Run automatically when the component mounts
  • Used with useQuery hook

Mutations (what you'll learn now)

  • Modify data on the server (create, update, delete)
  • Run only when you call them (like clicking a button)
  • Used with useMutation hook
  • Can update the cache after completion

Explore the Demo App

Take a look at the app at http://localhost:5173:

You'll see a "My Pokemon Favorites" section with:

  • โœ… A form to add favorites (but it doesn't work yet!)
  • โœ… A list area that shows "No favorites yet - add your Apollo hooks to see data!"

Your job is to make the form actually save data and display the favorites list!

Step 1: Understanding Our Local API

Our app uses a local JSON server for favorites. The favorites have this structure:

type Favorite = {
  id: string // Unique identifier
  title: string // Pokemon name
  body: string // Why it's your favorite
  createdAt: string // When it was added
}

๐Ÿง  Why a separate API? We're using a local JSON server to simulate a real backend. This teaches you how to work with different GraphQL endpoints - a common real-world scenario!

Step 2: Understanding the Component Setup

Open src/components/MyFavorites.tsx. You'll see it already has:

  • โœ… The form with state management (title and body state)
  • โœ… The UI structure for displaying favorites
  • โœ… Apollo Client imports (gql, useQuery, favoritesClient)
  • โœ… A working useQuery hook that fetches favorites
  • โŒ Missing the mutation functionality

๐Ÿง  What's already set up?

  • useQuery: Already fetching favorites from the local API
  • favoritesClient: A separate Apollo Client instance for our local API (different from the Pokemon API)
  • The favorites list should already be working (showing "No favorites yet" since the database is empty)

๐Ÿ’พ Save and check: You should see the favorites list working, but the form doesn't save data yet.

Step 3: Add Your GraphQL Mutation

You'll notice the component already has the GET_FAVORITES query implemented - that's why you can see the favorites list working! Now you need to add the mutation for creating new favorites.

๐ŸŽฏ Your Task: Add the mutation. Replace the "TODO: Add your CREATE_FAVORITE mutation here" comment with:

const CREATE_FAVORITE = gql`
  mutation CreateFavorite($input: CreateFavoriteInput!) {
    createFavorite(input: $input) {
      id
      title
      body
      createdAt
    }
  }
`

๐Ÿง  What's happening here?

The Mutation:

  • CREATE_FAVORITE: Creates a new favorite
  • $input: CreateFavoriteInput!: Takes an input object with title and body
  • Returns the created favorite with all its fields (including the generated id and createdAt)
  • This follows the same pattern as the Pokemon query, but for creating data instead of reading it

๐Ÿ’พ Save and check: Still showing the same behavior - we haven't connected the mutation hook yet!

Step 4: Add useMutation to Your Imports

Before we can use mutations, we need to import the useMutation hook. Notice the component currently only imports useQuery.

๐ŸŽฏ Your Task: Update your imports to include useMutation:

import { gql, useQuery, useMutation } from "@apollo/client"

๐Ÿง  What's useMutation?

  • useMutation: A React hook for executing GraphQL mutations
  • Unlike useQuery, it doesn't run automatically - you call it when needed (like on form submit)
  • Returns a function to execute the mutation plus loading/error states

๐Ÿ’พ Save and check: Nothing should change visually yet - we just added the import!

Step 5: Understanding the Pre-implemented Query

Notice that the useQuery hook is already implemented in the component! Let's understand what it's doing:

const {
  data,
  loading: queryLoading,
  error: queryError,
} = useQuery<{
  favorites: Favorite[]
}>(GET_FAVORITES, {
  client: favoritesClient,
})

๐Ÿง  What's different from Part 2?

  • client: favoritesClient: We specify which Apollo Client to use (our local API, not the Pokemon API)
  • loading: queryLoading: We rename loading to avoid conflicts with mutation loading
  • No refetch for now - we'll add that later!

๐Ÿ’พ Save and check: You should already see "Loading favorites..." briefly, then "No favorites yet" (since there are no favorites in the database yet)!

Step 6: Add the useMutation Hook

Now for the exciting part - adding the mutation! This is where useMutation differs significantly from useQuery.

๐ŸŽฏ Your Task: Replace the "TODO: Add your useMutation hook for creating favorites" comment with:

const [createFavorite, { loading: mutationLoading, error: mutationError }] =
  useMutation<
    { createFavorite: Favorite },
    { input: { title: string; body: string } }
  >(CREATE_FAVORITE, {
    client: favoritesClient,
  })

๐Ÿง  What's useMutation doing?

Different Return Pattern:

  • useQuery returns { data, loading, error }
  • useMutation returns [mutationFunction, { loading, error, data }]

The Mutation Function:

  • createFavorite: A function you call to execute the mutation
  • Only runs when YOU call it (not automatically like queries)

TypeScript Generics:

  • First type: What the mutation returns ({ createFavorite: Favorite })
  • Second type: What variables it needs ({ input: { title: string; body: string } })

Options:

  • client: favoritesClient: Use our local API client

๐Ÿ’พ Save and check: Form should now show proper loading states, but still won't save data!

Step 7: Update the State Variables

๐ŸŽฏ Your Task: Remove the placeholder mutation state variables.

Delete these lines:

const mutationLoading = false
const mutationError = null

๐Ÿง  What about the other variables?

  • favorites is already correctly implemented as const favorites = data?.favorites || []
  • queryLoading and queryError come from the useQuery hook (renamed to avoid conflicts)
  • We only need to remove the placeholder mutation variables

๐Ÿง  Why data?.favorites || []?

  • data might be undefined while loading
  • ?. safely accesses favorites even if data is undefined
  • || [] provides an empty array as fallback

๐Ÿ’พ Save and check: You should see the favorites list area, but still can't add favorites!

Step 8: Connect the Mutation to the Form

๐ŸŽฏ Your Task: Update the handleSubmit function. Replace the TODO comment with:

try {
  await createFavorite({
    variables: { input: { title, body } },
  })
} catch (err) {
  console.error("Failed to create favorite:", err)
}

๐Ÿง  What's happening here?

  • await createFavorite(): Call the mutation function and wait for it to complete
  • variables: { input: { title, body } }: Pass the form data as GraphQL variables
  • The structure matches our mutation: $input: CreateFavoriteInput!
  • try/catch: Handle any errors that might occur

๐Ÿ’พ Save and check: ๐ŸŽ‰ Try adding a favorite! But... it won't appear in the list until you refresh the page. That's our next step!

Step 9: Add Cache Updates with onCompleted

The mutation works, but the UI doesn't update automatically. We need to tell Apollo to refresh the favorites list after creating a new one.

๐ŸŽฏ Your Task: Update your useMutation hook to include onCompleted:

const [createFavorite, { loading: mutationLoading, error: mutationError }] =
  useMutation<
    { createFavorite: Favorite },
    { input: { title: string; body: string } }
  >(CREATE_FAVORITE, {
    client: favoritesClient,
    onCompleted: () => {
      setTitle("") // Clear the form
      setBody("")
    },
  })

๐Ÿง  What's onCompleted?

  • A callback function that runs after the mutation succeeds
  • Perfect place to update UI state or clear forms
  • Runs only on success (not on errors)

๐Ÿ’พ Save and check: Now the form clears after adding, but the list still doesn't update automatically!

Step 10: Add Refetch for Immediate Updates

To see new favorites immediately, we need to refetch the favorites list after creating one.

๐ŸŽฏ Your Task: First, update your useQuery to include refetch:

const {
  data,
  loading: queryLoading,
  error: queryError,
  refetch,
} = useQuery<{
  favorites: Favorite[]
}>(GET_FAVORITES, {
  client: favoritesClient,
})

๐ŸŽฏ Your Task: Then, update your useMutation to call refetch:

const [createFavorite, { loading: mutationLoading, error: mutationError }] =
  useMutation<
    { createFavorite: Favorite },
    { input: { title: string; body: string } }
  >(CREATE_FAVORITE, {
    client: favoritesClient,
    onCompleted: () => {
      refetch() // Refresh the favorites list
      setTitle("") // Clear the form
      setBody("")
    },
  })

๐Ÿง  What's refetch doing?

  • refetch(): Re-runs the GET_FAVORITES query
  • Fetches fresh data from the server
  • Updates the cache with the latest favorites
  • Triggers a re-render with the new data

๐Ÿ’พ Save and check: ๐ŸŽ‰ Now try adding a favorite - it should appear immediately!

Step 11: Test Your Complete Mutation Flow

Save your file and test the complete functionality!

๐ŸŽฏ Try This:

  1. Add a favorite Pokemon with a reason (e.g., "Pikachu" - "So cute and electric!")
  2. Watch it appear in the list immediately
  3. Add another favorite
  4. Refresh the page - do they persist?
  5. Try submitting an empty form - what happens?

๐Ÿง  What You Just Built: You've created a complete mutation flow! Here's what Apollo Client is doing for you:

  1. Form Submission: createFavorite() sends a GraphQL mutation to the server
  2. Loading States: mutationLoading shows "Adding..." while the request is in progress
  3. Error Handling: mutationError catches and displays any problems
  4. Cache Updates: refetch() ensures the UI shows the latest data
  5. UX Polish: onCompleted clears the form for the next entry

Discussion Questions:

  • Why don't mutations run automatically like queries?
  • What's the difference between queryLoading and mutationLoading?
  • Why do we need refetch() after the mutation?
  • How does the onCompleted callback improve user experience?
  • What would happen if we forgot to clear the form?

๐Ÿš€ Advanced Challenge: Instead of using refetch() (which makes an extra network request), can you think of a more efficient way to update the cache? (Hint: Look up Apollo's update function - but that's for later!)


Part 4: Advanced Patterns & Best Practices ๐Ÿš€

๐Ÿ“– Further Reading

This section covers advanced topics. For deeper understanding, explore:

Cache Management Strategies

Apollo Client provides several ways to update the cache after mutations:

  1. refetchQueries (easiest, but makes extra network requests)
  2. update function (more efficient, updates cache directly)
  3. optimisticResponse (best UX, updates UI immediately)

Example: Optimistic Updates

const [createFavorite] = useMutation(CREATE_FAVORITE, {
  optimisticResponse: {
    createFavorite: {
      __typename: "Favorite",
      id: "temp-id",
      title: pokemonName,
      body: reason,
      createdAt: new Date().toISOString(),
    },
  },
  update(cache, { data }) {
    // Update cache manually instead of refetching
    const existingFavorites = cache.readQuery({ query: GET_FAVORITES })
    cache.writeQuery({
      query: GET_FAVORITES,
      data: {
        favorites: [data.createFavorite, ...existingFavorites.favorites],
      },
    })
  },
})

Error Handling Best Practices

const { data, loading, error } = useQuery(GET_POKEMON, {
  variables: { pokemon: searchTerm },
  errorPolicy: "all", // Return partial data even on errors
  onError: (error) => {
    // Log errors for debugging
    console.error("Pokemon query error:", error)
  },
})

Performance Tips

  1. Use skip to avoid unnecessary queries
  2. Memoize variables to prevent re-renders
  3. Use fragments for reusable query parts
  4. Implement proper loading states for better UX

๐ŸŽ‰ Congratulations!

You've successfully learned the basics of:

  • โœ… Setting up and using Apollo Client
  • โœ… Writing GraphQL queries and using useQuery
  • โœ… Handling loading states and errors
  • โœ… Creating mutations with useMutation
  • โœ… Managing cache updates
  • โœ… Building a React app with GraphQL

Next Steps

  • Explore Apollo Client's subscriptions for real-time data
  • Learn about advanced caching strategies
  • Try Apollo Client's local state management
  • Build more complex apps with nested queries and mutations

Resources


Part 5: Optional Practice - Build Your Own Feature! ๐ŸŽจ

๐ŸŽ‰ Congratulations! You've learned the fundamentals of Apollo Client. Now it's time to get creative and build something uniquely yours!

Using everything you've learned about useQuery and useMutation (or just one of them!), create an original feature that combines Pokemon data with your own creative twist. This is your chance to experiment, have fun, and see what cool ideas you can bring to life!

๐Ÿ’ก Feature Ideas

Choose one of these ideas (or create your own!):

๐Ÿ† Pokemon Team Builder

  • Search and add Pokemon to build your dream team (limit to 6)
  • Display team stats and type coverage
  • Save multiple teams with custom names

โญ Enhanced Favorites with Ratings

  • Add a rating system (1-5 stars) to your favorites
  • Sort favorites by rating, name, or date added
  • Add categories like "Cute", "Strong", "Legendary"

๐ŸŽฎ Pokemon Battle Simulator

  • Create a simple battle system using base stats
  • Let users pick two Pokemon and simulate a battle
  • Save battle history and win/loss records

Remember, there's no "right" way to do this - just pick something that sounds fun and start building! You can always start simple and add more features as you go.

Happy coding! ๐Ÿš€

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published