Understanding the Problem
This exercise is about using React context to share state between components. We need to:
- Connect React components to a context
- Display the content from the context
- Change the context from a component
- Create a custom context hook
- Use tests to verify our implementation
Whiteboard Plan
- Implement SelectedCoffeeBean component (Phase 1)
- Import useContext and CoffeeContext
- Use context to get the coffeeBean
- Display the name in the h2 element
- Add components to App.jsx
- Import SelectedCoffeeBean
- Import SetCoffeeBean
- Render both components in App
- 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
- 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:
- We import
useContexthook from React to access our context - We import
CoffeeContextto access our coffee context - We destructure
coffeeBeanfrom the context object - We display the
coffeeBean.namein the h2 element
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:
- We import both
SelectedCoffeeBeanandSetCoffeeBeancomponents - We render both components in the App component
- We pass the
coffeeBeansarray to theSetCoffeeBeancomponent as a prop
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:
- We import
useContextandCoffeeContextlike before - We destructure
coffeeBeanandsetCoffeeBeanIdfrom the context - We create a
handleChangefunction to callsetCoffeeBeanIdwith the new value - We make the select a controlled component by:
- Setting
value={coffeeBean.id}to reflect the current state - Adding
onChange={handleChange}to update the state when changed
- Setting
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:
- We add the
useContextimport - We create a new
useCoffeecustom hook that:- Calls
useContextwith ourCoffeeContext - Adds error checking to ensure it's used within a provider
- Returns the context value
- Calls
- This hook makes our components cleaner and ensures proper error handling
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:
- Run
npm test 1-SelectedCoffeeBeanto test Phase 1 - Run
npm test 2-SetCoffeeBeanto test Phase 2 - Run
npm testto 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:
- Theme Switching: Providing dark/light mode across your entire application
- User Authentication: Sharing user login state across components
- Language Settings: Managing internationalization settings
- Shopping Cart: Managing cart items across an e-commerce app
- Application Settings: Storing user preferences
Custom hooks like our useCoffee hook are a best practice in React development because they:
- Encapsulate complex logic in reusable functions
- Make components cleaner and more focused
- Provide better error handling
- Make testing easier
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.