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.
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 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 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 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:
Cleanup Functions
Empty Dependencies
With Dependencies
useEffect (() => {
const timer = setTimeout ( onTimeout , timeout );
// Cleanup function runs when component unmounts
// or before effect runs again
return () => {
clearTimeout ( timer );
};
}, [ timeout , onTimeout ]);
// Runs once on mount, cleanup on unmount
useEffect (() => {
const interval = setInterval (() => {
setRemainingTime (( prev ) => prev - 100 );
}, 100 );
return () => {
clearInterval ( interval );
};
}, []); // Empty array = run once
// Runs when dependencies change
useEffect (() => {
console . log ( 'Question changed' );
// Reset state or perform actions
}, [ activeQuestionIndex , selectedAnswer ]);
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:
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:
Timeout Effect
Triggers the skip/advance action after the full timeout period useEffect (() => {
const timer = setTimeout ( onTimeout , timeout );
return () => clearTimeout ( timer );
}, [ timeout , onTimeout ]);
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:
Unmounts the old component (running cleanup)
Mounts a new component (running effects)
This pattern is perfect for resetting timers and state when moving between questions.
Question Data Structure
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
Basic Structure
Set up the Quiz component with Header and basic question display
State Management
Add state for tracking user answers and derive active question index
Question Output
Dynamically render questions based on active index
Answer Shuffling
Implement logic to shuffle answers randomly
Timer Implementation
Create QuestionTimer with useEffect and cleanup
Effect Dependencies
Properly configure useEffect dependencies with useCallback
Component Logic Separation
Extract Question component with its own state and logic
Summary Screen
Display quiz results with correct/wrong/skipped breakdown
Common Patterns & Solutions
Effect Cleanup
Why Cleanup Functions Matter
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:
Before the effect runs again (when dependencies change)
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 States
Progress Bar
.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