React Context: Managing Global State for Puppy Photos

Understanding the Problem

In our current React application, we have:

The challenge is that these components need to share state - when a user selects a puppy in the PupForm, the PupImage component should update to display that puppy. Since these components aren't directly parent/child to each other, passing props would be cumbersome. React Context provides a cleaner solution for this.

Devising a Plan

  1. Create a PupContext to store the selected puppy image
  2. Create a PupProvider component to manage the state for our context
  3. Wrap our application with the PupProvider
  4. Update PupImage to consume the context and display the selected puppy
  5. Update PupForm to update the context when a user selects a puppy
  6. Create a custom hook to simplify context consumption

Carrying Out the Plan

Step 1: Creating the PupContext

First, let's create a new directory for our context and create the PupContext file:

// src/context/PupContext.jsx

import { createContext } from 'react';

export const PupContext = createContext();

This creates a new React context that components can use to access and update the selected puppy.

Step 2: Creating the PupProvider

Now, let's create a provider component that will manage the state for our context:

// src/context/PupContext.jsx

import { createContext, useState } from 'react';
import speedy from '../pups/speedy-pup.jpg';

export const PupContext = createContext();

export function PupProvider(props) {
  const [puppyType, setPuppyType] = useState(speedy);

  return (
    <PupContext.Provider value={{ puppyType, setPuppyType }}>
      {props.children}
    </PupContext.Provider>
  );
}

This provider:

Step 3: Wrapping the App with PupProvider

Let's update the entry file to wrap our App with the PupProvider:

// src/main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { PupProvider } from './context/PupContext';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <PupProvider>
      <App />
    </PupProvider>
  </React.StrictMode>
);

Now all components inside App will have access to the PupContext.

Step 4: Updating PupImage to Use Context

Now let's update the PupImage component to use the context:

// src/components/PupImage/PupImage.jsx

import { useContext } from 'react';
import { PupContext } from '../../context/PupContext';
import speedy from '../../pups/speedy-pup.jpg';
import banana from '../../pups/banana-pup.jpg';
import sleepy from '../../pups/sleepy-pup.jpg';

const PupImage = () => {
  const { puppyType } = useContext(PupContext);

  return (
    <img src={puppyType} alt="pup" />
  );
};

export default PupImage;

We're using the useContext hook to access the puppyType value from our context.

Step 5: Updating PupForm to Use Context

Next, let's update the PupForm component to update the context when a user selects a puppy:

// src/components/PupForm/PupForm.jsx

import { useState, useContext } from 'react';
import { PupContext } from '../../context/PupContext';
import banana from '../../pups/banana-pup.jpg';
import sleepy from '../../pups/sleepy-pup.jpg';
import speedy from '../../pups/speedy-pup.jpg';

function PupForm() {
  const { puppyType, setPuppyType } = useContext(PupContext);
  const [selectedPup, setSelectedPup] = useState(puppyType);

  const onSubmit = (e) => {
    e.preventDefault();
    setPuppyType(selectedPup);
  }

  return (
    <form onSubmit={onSubmit}>
      <select
        name="pup"
        onChange={e => setSelectedPup(e.target.value)}
        value={selectedPup}
      >
        <option value="select" disabled>Select a pup!</option>
        <option value={speedy}>Speedy Pup</option>
        <option value={banana}>Banana Pup</option>
        <option value={sleepy}>Sleepy Pup</option>
      </select>
      <button>
        Submit
      </button>
    </form>
  );
}

export default PupForm;

Now when the form is submitted, it will update the context value, which will cause the PupImage component to re-render with the new puppy.

Step 6: Creating a Custom Hook

Finally, let's create a custom hook to make it easier to use our context:

// src/context/PupContext.jsx

import { createContext, useState, useContext } from 'react';
import speedy from '../pups/speedy-pup.jpg';

export const PupContext = createContext();

export function PupProvider(props) {
  const [puppyType, setPuppyType] = useState(speedy);

  return (
    <PupContext.Provider value={{ puppyType, setPuppyType }}>
      {props.children}
    </PupContext.Provider>
  );
}

export function usePuppyType() {
  return useContext(PupContext);
}

Now let's update our components to use the custom hook:

Updated PupImage

// src/components/PupImage/PupImage.jsx

import { usePuppyType } from '../../context/PupContext';
import speedy from '../../pups/speedy-pup.jpg';
import banana from '../../pups/banana-pup.jpg';
import sleepy from '../../pups/sleepy-pup.jpg';

const PupImage = () => {
  const { puppyType } = usePuppyType();
  
  return (
    <img src={puppyType} alt="pup" />
  );
};

export default PupImage;

Updated PupForm

// src/components/PupForm/PupForm.jsx

import { useState } from 'react';
import { usePuppyType } from '../../context/PupContext';
import banana from '../../pups/banana-pup.jpg';
import sleepy from '../../pups/sleepy-pup.jpg';
import speedy from '../../pups/speedy-pup.jpg';

function PupForm() {
  const { puppyType, setPuppyType } = usePuppyType();
  const [selectedPup, setSelectedPup] = useState(puppyType);

  const onSubmit = (e) => {
    e.preventDefault();
    setPuppyType(selectedPup);
  }

  return (
    <form onSubmit={onSubmit}>
      <select
        name="pup"
        onChange={e => setSelectedPup(e.target.value)}
        value={selectedPup}
      >
        <option value="select" disabled>Select a pup!</option>
        <option value={speedy}>Speedy Pup</option>
        <option value={banana}>Banana Pup</option>
        <option value={sleepy}>Sleepy Pup</option>
      </select>
      <button>
        Submit
      </button>
    </form>
  );
}

export default PupForm;

Handling ESLint Warning

Since our PupContext.jsx file now exports multiple items (PupProvider and usePuppyType), you might receive an ESLint warning about "Fast refresh only works when a file only exports components." To solve this, update your .eslintrc.cjs file:

// .eslintrc.cjs
module.exports = {
  // ... other config
  
  overrides: [
    {
      files: ["src/context/*.jsx"],
      rules: {
        'react-refresh/only-export-components': 'off'
      }
    }
  ],
  
  // ... other config
}

Looking Back: Understanding React Context

Key Concepts

The Flow of Context

  1. Create a context using createContext()
  2. Create a provider component that manages state
  3. Wrap your application or component tree with the provider
  4. Consume the context in any component using useContext() or a custom hook
  5. Update the context from any component that has access to the setter function

When to Use Context

Context is great for:

But remember: Context is primarily for state that changes infrequently. For high-frequency updates, consider other state management solutions.

Real-World Applications

React Context is widely used in real-world applications for various purposes:

Many popular libraries leverage context internally, including:

Further Learning

To deepen your understanding of React Context, consider exploring: