React Context Practice

Understanding the Problem

In this exercise, we need to implement a coffee bean selection app using React Context. The application should allow users to:

We need to complete two main components:

  1. SelectedCoffeeBean - Shows the name of the currently selected coffee bean
  2. SetCoffeeBean - Allows the user to select a different coffee bean

Both components need to communicate through the provided CoffeeContext.

Finally, we'll refactor our solution to use a custom hook for accessing the context.

Devising a Plan

  1. Analyze the provided files to understand the structure of the application
  2. Complete Phase 1: Implement the SelectedCoffeeBean component
    • Update App.jsx to render SelectedCoffeeBean
    • Modify SelectedCoffeeBean.jsx to use the CoffeeContext
  3. Complete Phase 2: Implement the SetCoffeeBean component
    • Update App.jsx to render SetCoffeeBean and pass in coffee beans data
    • Modify SetCoffeeBean.jsx to use the CoffeeContext
  4. Complete Phase 3: Create a custom useCoffee hook
    • Add the hook to CoffeeContext.jsx
    • Refactor components to use the new hook

Carrying Out the Plan

Phase 1: SelectedCoffeeBean Component

Let's start by updating the App.jsx file to render the SelectedCoffeeBean component:

App.jsx

import coffeeBeans from './mockData/coffeeBeans.json';
import SelectedCoffeeBean from './components/SelectedCoffeeBean';

function App() {
  return (
    <>
      <h1>Welcome to Coffee App</h1>
      <SelectedCoffeeBean />
    </>
  );
}

export default App;

Now, let's update the SelectedCoffeeBean.jsx file to use the CoffeeContext:

SelectedCoffeeBean.jsx

import { useContext } from 'react';
import { CoffeeContext } from '../context/CoffeeContext';

const SelectedCoffeeBean = () => {
  const { coffeeBean } = useContext(CoffeeContext);
  
  return (
    <div className="selected-coffee">
      <h2>Current Selection: {coffeeBean.name}</h2>
    </div>
  );
}

export default SelectedCoffeeBean;

In this component, we're:

  1. Importing useContext from React
  2. Importing the CoffeeContext from our context file
  3. Using useContext to access the context's value
  4. Destructuring to get the coffeeBean from the context
  5. Displaying the coffeeBean.name inside the h2 tag

Phase 2: SetCoffeeBean Component

First, let's update App.jsx to render the SetCoffeeBean component:

App.jsx (updated)

import coffeeBeans from './mockData/coffeeBeans.json';
import SelectedCoffeeBean from './components/SelectedCoffeeBean';
import SetCoffeeBean from './components/SetCoffeeBean';

function App() {
  return (
    <>
      <h1>Welcome to Coffee App</h1>
      <SelectedCoffeeBean />
      <SetCoffeeBean coffeeBeans={coffeeBeans} />
    </>
  );
}

export default App;

Now, let's update the SetCoffeeBean.jsx component to use the CoffeeContext:

SetCoffeeBean.jsx

import { useContext } from 'react';
import { CoffeeContext } from '../context/CoffeeContext';

const SetCoffeeBean = ({ coffeeBeans }) => {
  const { coffeeBean, setCoffeeBeanId } = useContext(CoffeeContext);
  
  return (
    <div className="set-coffee-bean">
      <h2>Select a Coffee Bean</h2>
      <select
        name="coffee-bean"
        value={coffeeBean.id}
        onChange={(e) => setCoffeeBeanId(e.target.value)}
      >
        {coffeeBeans.map(bean => (
          <option
            key={bean.id}
            value={bean.id}
          >
            {bean.name}
          </option>
        ))}
      </select>
    </div>
  );
}

export default SetCoffeeBean;

In this component, we're:

  1. Importing useContext and CoffeeContext
  2. Using useContext to access both coffeeBean and setCoffeeBeanId
  3. Setting the value of the select to coffeeBean.id to make it a controlled component
  4. Adding an onChange handler that calls setCoffeeBeanId with the new value

Phase 3: Creating a Custom Hook

Let's create a custom useCoffee hook in the CoffeeContext.jsx file:

CoffeeContext.jsx (updated)

import { createContext, useState, useContext } from 'react';
import coffeeBeans from '../mockData/coffeeBeans.json';

export const CoffeeContext = createContext();

export const useCoffee = () => {
  const context = useContext(CoffeeContext);
  if (!context) {
    throw new Error('useCoffee must be used within a CoffeeProvider');
  }
  return context;
};

export default function CoffeeProvider(props) {
  const [coffeeBean, setCoffeeBean] = useState(coffeeBeans[0]);

  const setCoffeeBeanId = (coffeeBeanId) => {
    const bean = coffeeBeans.find(bean => {
      return Number(bean.id) === Number(coffeeBeanId)
    });
    setCoffeeBean(bean);
  };

  return (
    <CoffeeContext.Provider
      value={{
        coffeeBean,
        setCoffeeBeanId
      }}
    >
      {props.children}
    </CoffeeContext.Provider>
  );
}

Now let's update the SelectedCoffeeBean.jsx component to use our custom hook:

SelectedCoffeeBean.jsx (refactored)

import { useCoffee } from '../context/CoffeeContext';

const SelectedCoffeeBean = () => {
  const { coffeeBean } = useCoffee();
  
  return (
    <div className="selected-coffee">
      <h2>Current Selection: {coffeeBean.name}</h2>
    </div>
  );
}

export default SelectedCoffeeBean;

Let's also update the SetCoffeeBean.jsx component:

SetCoffeeBean.jsx (refactored)

import { useCoffee } from '../context/CoffeeContext';

const SetCoffeeBean = ({ coffeeBeans }) => {
  const { coffeeBean, setCoffeeBeanId } = useCoffee();
  
  return (
    <div className="set-coffee-bean">
      <h2>Select a Coffee Bean</h2>
      <select
        name="coffee-bean"
        value={coffeeBean.id}
        onChange={(e) => setCoffeeBeanId(e.target.value)}
      >
        {coffeeBeans.map(bean => (
          <option
            key={bean.id}
            value={bean.id}
          >
            {bean.name}
          </option>
        ))}
      </select>
    </div>
  );
}

export default SetCoffeeBean;

Looking Back

What We've Accomplished

We've successfully implemented a coffee bean selection app using React Context. The key achievements include:

  1. Created a component that displays the currently selected coffee bean
  2. Created a component that allows users to change the selected coffee bean
  3. Ensured both components communicate through the React Context
  4. Refactored our code to use a custom hook for better organization and reusability

How Context Works in This App

Let's break down how context is working in our application:

  1. The CoffeeContext is created using createContext()
  2. The CoffeeProvider component manages the state (coffee bean selection) and provides functions to update it
  3. The entire app is wrapped in the CoffeeProvider in main.jsx
  4. Components access the context using either useContext(CoffeeContext) or our custom useCoffee() hook
  5. When a user selects a new coffee bean, the context is updated, which automatically updates all components using that context

Benefits of Using a Custom Hook

Creating a custom useCoffee hook offers several advantages:

  1. Encapsulation: The hook encapsulates the details of how we access the context
  2. Error handling: We can add validation to ensure the hook is used correctly
  3. Cleaner components: Components don't need to import both useContext and CoffeeContext
  4. DRY principle: We avoid repeating the same context access pattern across multiple components
  5. Better maintainability: If we change how context is accessed, we only need to update the hook

Real-World Applications

This pattern of using context with a custom hook is common in many real-world React applications, such as:

Common Mistakes and Debugging Tips

Further Learning

  1. Explore context optimizations using useMemo and useCallback
  2. Learn about using multiple contexts in a single application
  3. Compare context with other state management solutions like Redux or Zustand
  4. Investigate TypeScript with React context for better type safety
  5. Explore more advanced patterns like context composition or context splitting