In this project, we need to complete a React application that displays a turkey with customizable features. The application allows users to:
The main focus is implementing useEffect hooks to respond to state changes and update other state variables accordingly. This is a common pattern in React applications where derived state needs to be calculated based on other state values.
useEffect hook in App.jsx to update featherColors based on checkbox stateuseEffect in PictureDisplay.jsxuseEffect in Message.jsxFirst, let's implement the useEffect hook to update the featherColors state based on checkbox selections.
We need to modify App.jsx to import useEffect and add our hook.
// App.jsx
import { useState, useEffect } from 'react';
import Message from './components/Message';
import PictureDisplay from './components/PictureDisplay';
function App() {
// State variables remain the same
const [size, setSize] = useState('s');
const [featherCount, setFeatherCount] = useState(0);
const [featherColors, setFeatherColors] = useState([]);
const [isRed, setIsRed] = useState(false);
const [isOrange, setIsOrange] = useState(false);
const [isBrown, setIsBrown] = useState(false);
const [isLightBrown, setIsLightBrown] = useState(false);
const [isYellow, setIsYellow] = useState(false);
// Add useEffect hook to update featherColors based on checkbox states
useEffect(() => {
// For debugging
console.log('Color Change :: red?', isRed);
console.log('Color Change :: orange?', isOrange);
console.log('Color Change :: brown?', isBrown);
console.log('Color Change :: light brown?', isLightBrown);
console.log('Color Change :: yellow?', isYellow);
const colors = [];
if (isRed) colors.push('red');
if (isOrange) colors.push('orange');
if (isBrown) colors.push('brown');
if (isLightBrown) colors.push('light-brown');
if (isYellow) colors.push('yellow');
setFeatherColors(colors);
}, [isRed, isOrange, isBrown, isLightBrown, isYellow]);
return (
// JSX remains the same
);
}
export default App;
This useEffect hook runs whenever any of the checkbox states change. It builds an array of color names based on which checkboxes are checked, then updates the featherColors state, which will be passed as a prop to the PictureDisplay component.
Now we need to modify the PictureDisplay component to respond to size changes:
// PictureDisplay.jsx
import { useEffect, useState } from 'react';
import turkey from '../images/turkey.png';
import feather1 from '../images/feather1.svg';
// Other feather imports remain the same
const feathers = [
feather1,
feather2,
feather3,
feather4,
feather5,
feather6,
feather7,
feather8,
feather9,
featherA,
];
function PictureDisplay ({ size, featherCount, featherColors }) {
// Add state for size class
const [sizeClass, setSizeClass] = useState('');
// Debugging useEffect - can be commented out after testing
useEffect(() => {
console.log('PictureDisplay', size, featherCount, featherColors);
}, [size, featherCount, featherColors]);
// Size calculation useEffect
useEffect(() => {
console.log('PictureDisplay size', size);
let cname = '';
switch (size) {
case 'm':
cname = 'medium';
break;
case 'l':
cname = 'large';
break;
case 'xl':
cname = 'xlarge';
break;
default:
cname = 'small';
break;
}
setSizeClass(cname);
}, [size]);
// Feather count debugging useEffect
useEffect(() => {
console.log('PictureDisplay feather count', featherCount);
}, [featherCount]);
// Feather colors debugging useEffect
useEffect(() => {
console.log('PictureDisplay feather colors', featherColors);
}, [featherColors]);
// TODO: Wrap in useEffect
const colors = [];
if (!featherColors || featherColors.length === 0) featherColors = [''];
for (let i=0; i<featherCount; i++) {
colors.push(featherColors[i % featherColors.length]);
}
return (
<div className={`image-area ${sizeClass}`}>
{colors.map((c, i) =>
<img
key={feathers[i]}
src={feathers[i]}
className={`image-feather ${c}`}
alt=""
/>
)}
<img src={turkey} className="image-turkey" alt="turkey" />
</div>
);
}
export default PictureDisplay;
This adds a state variable for the size class and a useEffect that updates it whenever the size prop changes. We also update the className on the div to use this state variable.
Now let's do the same for the Message component:
// Message.jsx
import { useEffect, useState } from 'react';
function Message({ size }) {
// Add state for size class
const [sizeClass, setSizeClass] = useState('');
// Size calculation useEffect
useEffect(() => {
console.log('Message size', size);
let cname = '';
switch (size) {
case 'm':
cname = 'medium';
break;
case 'l':
cname = 'large';
break;
case 'xl':
cname = 'xlarge';
break;
default:
cname = 'small';
break;
}
setSizeClass(cname);
}, [size]);
return (
<div className={`message ${sizeClass}`}>
(Oh my! Your bird is naked!)
</div>
);
}
export default Message;
Similar to PictureDisplay, we add a state variable for the size class and a useEffect to update it based on the size prop.
First, we need to modify App.jsx to pass featherCount to the Message component:
// In App.jsx, update the Message component:
<Message size={size} featherCount={featherCount} />
Now update the Message component to display different messages based on featherCount:
// Message.jsx
import { useEffect, useState } from 'react';
function Message({ size, featherCount }) {
const [sizeClass, setSizeClass] = useState('');
const [message, setMessage] = useState('');
// Size calculation useEffect
useEffect(() => {
console.log('Message size', size);
let cname = '';
switch (size) {
case 'm':
cname = 'medium';
break;
case 'l':
cname = 'large';
break;
case 'xl':
cname = 'xlarge';
break;
default:
cname = 'small';
break;
}
setSizeClass(cname);
}, [size]);
// Message content useEffect
useEffect(() => {
if (featherCount <= 0)
setMessage('Oh my! Your bird is naked!');
else if (featherCount >= 10) {
setMessage('Full turkey!');
} else {
setMessage('Coming along...');
}
}, [featherCount]);
return (
<div className={`message ${sizeClass}`}>
{message}
</div>
);
}
export default Message;
This adds a new state variable for the message and a useEffect that updates it based on the featherCount prop.
Let's update PictureDisplay.jsx to wrap the feather colors code in a useEffect:
// In PictureDisplay.jsx, replace the TODO with:
useEffect(() => {
const colors = [];
if (!featherColors || featherColors.length === 0) featherColors = [''];
for (let i=0; i<featherCount; i++) {
colors.push(featherColors[i % featherColors.length]);
}
setDisplayColors(colors);
}, [featherCount, featherColors]);
// Add a state variable for display colors at the top of the component:
const [displayColors, setDisplayColors] = useState([]);
// Then update the return statement to use displayColors instead of colors:
return (
<div className={`image-area ${sizeClass}`}>
{displayColors.map((c, i) =>
<img
key={feathers[i]}
src={feathers[i]}
className={`image-feather ${c}`}
alt=""
/>
)}
<img src={turkey} className="image-turkey" alt="turkey" />
</div>
);
This wraps the feather colors calculation in a useEffect and adds a state variable to hold the results.
Let's refactor by moving the size class calculation to App.jsx:
// App.jsx - Add a new state variable and useEffect
const [sizeClass, setSizeClass] = useState('');
useEffect(() => {
let cname = '';
switch (size) {
case 'm':
cname = 'medium';
break;
case 'l':
cname = 'large';
break;
case 'xl':
cname = 'xlarge';
break;
default:
cname = 'small';
break;
}
setSizeClass(cname);
}, [size]);
// Then update the components to use sizeClass instead of size:
<PictureDisplay
sizeClass={sizeClass}
featherCount={featherCount}
featherColors={featherColors}
/>
<Message sizeClass={sizeClass} featherCount={featherCount} />
Now update PictureDisplay.jsx and Message.jsx to use sizeClass directly:
// PictureDisplay.jsx - Update props and remove redundant code
function PictureDisplay ({ sizeClass, featherCount, featherColors }) {
// Remove sizeClass state and useEffect for size calculation
// Rest of the code remains the same, just use sizeClass directly
return (
<div className={`image-area ${sizeClass}`}>
{/* rest of the JSX remains the same */}
</div>
);
}
// Message.jsx - Update props and remove redundant code
function Message({ sizeClass, featherCount }) {
// Remove sizeClass state and useEffect for size calculation
const [message, setMessage] = useState('');
// Keep the message useEffect but remove the size useEffect
return (
<div className={`message ${sizeClass}`}>
{message}
</div>
);
}
In App.jsx, update the size state initialization:
const [size, setSize] = useState('m'); // Change from 's' to 'm'
Update the size buttons in App.jsx to reflect the selection:
<div className="button-controls">
Size:
<button onClick={() => setSize('s')} disabled={size === 's'}>Small</button>
<button onClick={() => setSize('m')} disabled={size === 'm'}>Medium</button>
<button onClick={() => setSize('l')} disabled={size === 'l'}>Large</button>
<button onClick={() => setSize('xl')} disabled={size === 'xl'}>X-Large</button>
</div>
Convert the feather count input into a controlled component in App.jsx:
<div className="button-controls">
Feather Count:
<input
type="number"
onChange={(e) => {
const count = parseInt(e.currentTarget.value, 10);
if (count >= 0 && count <= 10) {
setFeatherCount(count);
}
}}
value={featherCount}
min={0}
max={10}
/>
</div>
We can add CSS classes to improve the layout. Let's add a new layout to App.jsx:
<!-- Add a container around controls -->
<div className="controls-container">
<div className="control-group">
<h3>Size</h3>
<div className="button-controls">
<button onClick={() => setSize('s')} disabled={size === 's'}>Small</button>
<button onClick={() => setSize('m')} disabled={size === 'm'}>Medium</button>
<button onClick={() => setSize('l')} disabled={size === 'l'}>Large</button>
<button onClick={() => setSize('xl')} disabled={size === 'xl'}>X-Large</button>
</div>
</div>
<div className="control-group">
<h3>Feather Count</h3>
<div className="button-controls">
<input
type="number"
onChange={(e) => {
const count = parseInt(e.currentTarget.value, 10);
if (count >= 0 && count <= 10) {
setFeatherCount(count);
}
}}
value={featherCount}
min={0}
max={10}
/>
</div>
</div>
<div className="control-group">
<h3>Feather Colors</h3>
<div className="button-controls checkbox-controls">
<label><input
type="checkbox"
onChange={(e) => setIsRed(e.currentTarget.checked)}
/>Red</label>
<label><input
type="checkbox"
onChange={(e) => setIsOrange(e.currentTarget.checked)}
/>Orange</label>
<label><input
type="checkbox"
onChange={(e) => setIsBrown(e.currentTarget.checked)}
/>Brown</label>
<label><input
type="checkbox"
onChange={(e) => setIsLightBrown(e.currentTarget.checked)}
/>Light Brown</label>
<label><input
type="checkbox"
onChange={(e) => setIsYellow(e.currentTarget.checked)}
/>Golden Yellow</label>
</div>
</div>
</div>
Let's review what we've accomplished:
These changes demonstrate the power of React's useEffect hook for handling derived state and side effects in response to state changes.
The patterns we've learned in this project are common in real-world React applications:
These techniques are used in applications like:
The useEffect hook is like setting up an automatic response system in your components. Think of it as saying, "Whenever these specific things change, I want you to run this code."
The hook takes two arguments:
This is similar to how you might set up an automatic notification in real life: "Whenever the temperature goes above 80 degrees (the dependency), turn on the air conditioner (the effect)."
useEffect(() => { ... }, [])useEffect(() => { ... }, [someValue])useEffect(() => { ... }) (no dependencies array)useEffect(() => { return () => { ... } }, [])