Context Practice

Understanding the Problem

This exercise is about using React context to share state between components. We need to:

  1. Connect React components to a context
  2. Display the content from the context
  3. Change the context from a component
  4. Create a custom context hook
  5. Use tests to verify our implementation

Whiteboard Plan

  1. Implement SelectedCoffeeBean component (Phase 1)
    • Import useContext and CoffeeContext
    • Use context to get the coffeeBean
    • Display the name in the h2 element
  2. Add components to App.jsx
    • Import SelectedCoffeeBean
    • Import SetCoffeeBean
    • Render both components in App
  3. Implement SetCoffeeBean component (Phase 2)
    • Import useContext and CoffeeContext
    • Use context to get coffeeBean and setCoffeeBeanId
    • Make the select element a controlled component
    • Connect onChange to setCoffeeBeanId
  4. Create a custom useCoffee hook (Phase 3)
    • Define in CoffeeContext.jsx
    • Refactor SelectedCoffeeBean to use the custom hook
    • Refactor SetCoffeeBean to use the custom hook

Phase 1 Solution: SelectedCoffeeBean

File location: src/components/SelectedCoffeeBean.jsx


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

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

export default SelectedCoffeeBean;
            

Explanation:

Update App Component

File location: src/App.jsx


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;
            

Explanation:

Phase 2 Solution: SetCoffeeBean

File location: src/components/SetCoffeeBean.jsx


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

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

export default SetCoffeeBean;
            

Explanation:

Phase 3 Solution: Custom Hook

File location: src/context/CoffeeContext.jsx


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

export const CoffeeContext = createContext();

// Custom hook to use the coffee context
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>
  );
}
            

Explanation:

Refactored SelectedCoffeeBean

File location: src/components/SelectedCoffeeBean.jsx


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

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

export default SelectedCoffeeBean;
            

Refactored SetCoffeeBean

File location: src/components/SetCoffeeBean.jsx


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

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

export default SetCoffeeBean;
            

Running Tests

To verify our implementation:

  1. Run npm test 1-SelectedCoffeeBean to test Phase 1
  2. Run npm test 2-SetCoffeeBean to test Phase 2
  3. Run npm test to run all tests

All tests should pass after implementing all three phases.

Real-World Applications

React Context is incredibly useful in real-world applications for:

Custom hooks like our useCoffee hook are a best practice in React development because they:

Additional Understanding

Context vs. Props

Props are great for passing data down a few levels in your component tree. Context is better when data needs to be accessed by many components at different nesting levels.

Context vs. Redux

For smaller applications, Context is often simpler and sufficient. For larger applications with complex state management needs, Redux might be more appropriate.

Why Custom Hooks?

Custom hooks like useCoffee follow the principle of "Don't Repeat Yourself" (DRY). Instead of writing the same context access code in multiple components, we extract it into a reusable hook.