React Turkey Display Tutorial

Understanding the Problem

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.

Devising a Plan

  1. Implement a useEffect hook in App.jsx to update featherColors based on checkbox state
  2. Add size calculation using useEffect in PictureDisplay.jsx
  3. Add size calculation using useEffect in Message.jsx
  4. Implement the feather colors rendering in PictureDisplay.jsx
  5. Update the messaging based on feather count
  6. Complete the bonus tasks if time permits

Carrying Out the Plan: Phase 2 - Color Checkboxes

First, let's implement the useEffect hook to update the featherColors state based on checkbox selections.

Step 1: Add useEffect to App.jsx

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.

Phase 3: Adjusting Picture and Message Size

Step 2: Update PictureDisplay.jsx

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.

Step 3: Update Message.jsx

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.

Phase 4: User-Friendly Messaging

Step 4: Update Message Component to Display Different Messages

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.

Bonus A: Additional Practice with useEffect

Step 5: Wrap the Feather Colors Code in useEffect

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.

Bonus B: Refactoring

Step 6: Move Size Class Calculation to App.jsx

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>
  );
}

Bonus C: Additional Enhancements

Task 1: Set Default Size to Medium

In App.jsx, update the size state initialization:

const [size, setSize] = useState('m'); // Change from 's' to 'm'

Task 2: Size Button Reflecting Selection

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>

Task 3: Prevent Invalid Feather Count

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>

Task 4: Improve Layout for Settings

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>

Reviewing Our Solution

Let's review what we've accomplished:

  1. Implemented color checkbox functionality using a useEffect hook
  2. Added size calculation for both PictureDisplay and Message components
  3. Updated the Message component to show different messages based on feather count
  4. Improved the feather color rendering with useEffect
  5. Refactored the code to calculate size class in one place
  6. Added bonus enhancements for a better user experience

These changes demonstrate the power of React's useEffect hook for handling derived state and side effects in response to state changes.

Real-World Applications

The patterns we've learned in this project are common in real-world React applications:

These techniques are used in applications like:

Further Understanding

The useEffect Hook Explained

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:

  1. A function that contains the code to run (the "effect")
  2. An array of dependencies that trigger the effect when they change

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)."

Common useEffect Patterns

Best Practices for useEffect