Skip to main content

Project Overview

The React Quiz project from Section 13 demonstrates how to build an interactive quiz application that tests React concepts. This demo project focuses heavily on useEffect, timer management, and handling side effects properly.
React Quiz Application

Learning Objectives

useEffect Hook

Manage side effects and lifecycle events

Timer Management

Create and cleanup timers with useEffect

Derived State

Calculate values from existing state

useCallback

Optimize function references for dependencies

Key Features

Quiz Functionality

Quiz automatically advances through questions, tracking user answers
Quiz.jsx
import { useState, useCallback } from 'react';
import QUESTIONS from '../questions.js';
import Question from './Question.jsx';
import Summary from './Summary.jsx';

export default function Quiz() {
  const [userAnswers, setUserAnswers] = useState([]);

  const activeQuestionIndex = userAnswers.length;
  const quizIsComplete = activeQuestionIndex === QUESTIONS.length;

  const handleSelectAnswer = useCallback(function handleSelectAnswer(
    selectedAnswer
  ) {
    setUserAnswers((prevUserAnswers) => {
      return [...prevUserAnswers, selectedAnswer];
    });
  }, []);

  const handleSkipAnswer = useCallback(
    () => handleSelectAnswer(null),
    [handleSelectAnswer]
  );

  if (quizIsComplete) {
    return <Summary userAnswers={userAnswers} />
  }

  return (
    <div id="quiz">
      <Question
        key={activeQuestionIndex}
        index={activeQuestionIndex}
        onSelectAnswer={handleSelectAnswer}
        onSkipAnswer={handleSkipAnswer}
      />
    </div>
  );
}
Questions automatically skip if not answered within the time limit
QuestionTimer.jsx
import { useState, useEffect } from 'react';

export default function QuestionTimer({ timeout, onTimeout, mode }) {
  const [remainingTime, setRemainingTime] = useState(timeout);

  useEffect(() => {
    console.log('SETTING TIMEOUT');
    const timer = setTimeout(onTimeout, timeout);

    return () => {
      clearTimeout(timer);
    };
  }, [timeout, onTimeout]);

  useEffect(() => {
    console.log('SETTING INTERVAL');
    const interval = setInterval(() => {
      setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
    }, 100);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
    <progress
      id="question-time"
      max={timeout}
      value={remainingTime}
      className={mode}
    />
  );
}
Answers are randomly shuffled for each question to prevent pattern memorization
const shuffledAnswers = [...QUESTIONS[index].answers];
shuffledAnswers.sort(() => Math.random() - 0.5);
Visual feedback shows whether selected answers are correct or wrong
Question.jsx
function handleSelectAnswer(answer) {
  setAnswer({
    selectedAnswer: answer,
    isCorrect: null,
  });

  setTimeout(() => {
    setAnswer({
      selectedAnswer: answer,
      isCorrect: QUESTIONS[index].answers[0] === answer,
    });

    setTimeout(() => {
      onSelectAnswer(answer);
    }, 2000);
  }, 1000);
}

let answerState = '';

if (answer.selectedAnswer && answer.isCorrect !== null) {
  answerState = answer.isCorrect ? 'correct' : 'wrong';
} else if (answer.selectedAnswer) {
  answerState = 'answered';
}

Core Concepts

useEffect for Side Effects

Side effects are operations that interact with systems outside of React’s rendering flow, such as timers, API calls, or manual DOM manipulation.
The project demonstrates multiple useEffect patterns:
useEffect(() => {
  const timer = setTimeout(onTimeout, timeout);

  // Cleanup function runs when component unmounts
  // or before effect runs again
  return () => {
    clearTimeout(timer);
  };
}, [timeout, onTimeout]);

Derived State Pattern

Instead of storing computed values in state, derive them:
const [userAnswers, setUserAnswers] = useState([]);

// Derived values - no separate state needed
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
const selectedProject = projects.find(p => p.id === selectedProjectId);
Don’t store derived state! If a value can be calculated from existing state, compute it during render instead of storing it separately.

useCallback for Stable References

const handleSelectAnswer = useCallback(function handleSelectAnswer(
  selectedAnswer
) {
  setUserAnswers((prevUserAnswers) => {
    return [...prevUserAnswers, selectedAnswer];
  });
}, []); // No dependencies - function never changes

const handleSkipAnswer = useCallback(
  () => handleSelectAnswer(null),
  [handleSelectAnswer]  // Depends on handleSelectAnswer
);
useCallback memoizes function references, preventing unnecessary re-renders when functions are passed as props or used in effect dependencies.

Question Component Architecture

The Question component manages the answer selection flow:
Question.jsx
import { useState } from 'react';
import QuestionTimer from './QuestionTimer.jsx';
import Answers from './Answers.jsx';
import QUESTIONS from '../questions.js';

export default function Question({ index, onSelectAnswer, onSkipAnswer }) {
  const [answer, setAnswer] = useState({
    selectedAnswer: '',
    isCorrect: null,
  });

  // Different timeout durations based on answer state
  let timer = 10000;  // 10s to answer

  if (answer.selectedAnswer) {
    timer = 1000;  // 1s to check answer
  }

  if (answer.isCorrect !== null) {
    timer = 2000;  // 2s to show result
  }

  function handleSelectAnswer(answer) {
    setAnswer({
      selectedAnswer: answer,
      isCorrect: null,
    });

    // Show "checking" state for 1 second
    setTimeout(() => {
      setAnswer({
        selectedAnswer: answer,
        isCorrect: QUESTIONS[index].answers[0] === answer,
      });

      // Show result for 2 seconds, then move to next
      setTimeout(() => {
        onSelectAnswer(answer);
      }, 2000);
    }, 1000);
  }

  let answerState = '';

  if (answer.selectedAnswer && answer.isCorrect !== null) {
    answerState = answer.isCorrect ? 'correct' : 'wrong';
  } else if (answer.selectedAnswer) {
    answerState = 'answered';
  }

  return (
    <div id="question">
      <QuestionTimer
        key={timer}
        timeout={timer}
        onTimeout={answer.selectedAnswer === '' ? onSkipAnswer : null}
        mode={answerState}
      />
      <h2>{QUESTIONS[index].text}</h2>
      <Answers
        answers={QUESTIONS[index].answers}
        selectedAnswer={answer.selectedAnswer}
        answerState={answerState}
        onSelect={handleSelectAnswer}
      />
    </div>
  );
}

Timer Implementation

Multiple Effect Pattern

The QuestionTimer uses two separate effects:
1

Timeout Effect

Triggers the skip/advance action after the full timeout period
useEffect(() => {
  const timer = setTimeout(onTimeout, timeout);
  return () => clearTimeout(timer);
}, [timeout, onTimeout]);
2

Interval Effect

Updates the progress bar every 100ms
useEffect(() => {
  const interval = setInterval(() => {
    setRemainingTime((prev) => prev - 100);
  }, 100);
  return () => clearInterval(interval);
}, []);
Always cleanup timers! Failing to clear timers in cleanup functions can cause memory leaks and unexpected behavior.

Key Component Pattern

Resetting Components with Key Prop

<QuestionTimer
  key={timer}  // Changes = component remounts
  timeout={timer}
  onTimeout={answer.selectedAnswer === '' ? onSkipAnswer : null}
  mode={answerState}
/>
When the key prop changes, React:
  1. Unmounts the old component (running cleanup)
  2. Mounts a new component (running effects)
This pattern is perfect for resetting timers and state when moving between questions.

Question Data Structure

questions.js
export default [
  {
    id: 'q1',
    text: 'Which of the following definitions best describes React.js?',
    answers: [
      'A library to build user interfaces with help of declarative code.',
      'A library for managing state in web applications.',
      'A framework to build user interfaces with help of imperative code.',
      'A library used for building mobile applications only.',
    ],
  },
  // ... more questions
];
The first answer in the array is always the correct one. Answers are shuffled when displayed.

Project Structure

13-demo-project-react-quiz/
├── src/
   ├── components/
   ├── Answers.jsx         # Answer button list
   ├── Header.jsx          # Quiz header
   ├── Question.jsx        # Single question component
   ├── QuestionTimer.jsx   # Timer with progress bar
   ├── Quiz.jsx            # Main quiz logic
   └── Summary.jsx         # Results summary
   ├── questions.js            # Question data
   ├── App.jsx                 # App wrapper
   └── main.jsx                # Entry point
├── public/
   └── quiz-logo.png           # Quiz logo
└── index.html

Implementation Steps

1

Basic Structure

Set up the Quiz component with Header and basic question display
2

State Management

Add state for tracking user answers and derive active question index
3

Question Output

Dynamically render questions based on active index
4

Answer Shuffling

Implement logic to shuffle answers randomly
5

Timer Implementation

Create QuestionTimer with useEffect and cleanup
6

Effect Dependencies

Properly configure useEffect dependencies with useCallback
7

Component Logic Separation

Extract Question component with its own state and logic
8

Summary Screen

Display quiz results with correct/wrong/skipped breakdown

Common Patterns & Solutions

Effect Cleanup

Without cleanup, timers continue running after component unmounts:
// BAD - No cleanup
useEffect(() => {
  setTimeout(() => {
    console.log('This runs even after unmount!');
  }, 5000);
}, []);

// GOOD - With cleanup
useEffect(() => {
  const timer = setTimeout(() => {
    console.log('This is cancelled on unmount');
  }, 5000);
  
  return () => clearTimeout(timer);
}, []);
Cleanup functions run:
  1. Before the effect runs again (when dependencies change)
  2. When the component unmounts
useEffect(() => {
  console.log('Effect running');
  
  return () => {
    console.log('Cleanup running');
  };
}, [dependency]);

// Logs:
// Effect running
// [dependency changes]
// Cleanup running
// Effect running

Answer State Management

const [answer, setAnswer] = useState({
  selectedAnswer: '',    // Which answer was clicked
  isCorrect: null,       // null = checking, true/false = result
});

// State transitions:
// 1. Initial: { selectedAnswer: '', isCorrect: null }
// 2. Selected: { selectedAnswer: 'A library...', isCorrect: null }
// 3. Validated: { selectedAnswer: 'A library...', isCorrect: true }

Styling & Visual Feedback

The quiz uses CSS classes to show answer states:
<li className="answer">
  <button
    onClick={() => onSelect(answer)}
    className={answerState}  // '', 'answered', 'correct', or 'wrong'
    disabled={answerState !== ''}
  >
    {answer}
  </button>
</li>
.answer.answered {
  background-color: #ffd500;
  color: #2c2c2c;
}

.answer.correct {
  background-color: #5dd589;
  color: #2c2c2c;
}

.answer.wrong {
  background-color: #ff6b6b;
  color: white;
}

Testing Checklist

Key Takeaways

useEffect Mastery

Learn when and how to use effects with proper cleanup

Derived State

Calculate values instead of storing duplicates

Timer Management

Handle timeouts and intervals safely

Component Reset

Use key prop to reset component state

Next Steps

Food Order Project

Build with Context API and HTTP requests

Side Effects Deep Dive

Learn more about useEffect patterns

Build docs developers (and LLMs) love