Interactive Quiz App: Timers, State & Array Methods
State Machine banao, Reduce se count karo, Interval se time rakho, localStorage mein save karo!
Quiz apps have distinct phases: 'start', 'playing', and 'finished'. Never allow invalid transitions — a user shouldn't answer after time is up!
The data structure is crucial. You need an array of question objects, a separate answers map, and a current index tracker.
Do NOT mutate the original questions array. Always shuffle a copy using Fisher-Yates.
Keep score calculation separate from rendering — pure functions are easier to test and debug.
const state = {
questions: [], // Shuffled copy
currentIndex: 0,
answers: {}, // { 0: 'A', 1: 'C' }
status: 'start', // 'start' | 'playing' | 'finished'
timeLeft: 30
};
function startQuiz(rawQuestions) {
// Shuffle using Fisher-Yates
const shuffled = [...rawQuestions];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
state.questions = shuffled;
state.currentIndex = 0;
state.answers = {};
state.status = 'playing';
state.timeLeft = 30;
startTimer();
render();
}
state.status first. This pattern is used in game development and UI frameworks like XState.Use reduce to calculate the total score by comparing state.answers with state.questions.
Use map to render the review screen — transform each question into HTML showing correct/incorrect.
Use filter to extract only the incorrectly answered questions for focused review.
These methods make data transformation declarative and bug-free — no manual loops, no off-by-one errors.
// 1. Calculate Score using reduce
function calculateScore() {
return state.questions.reduce((score, q, index) => {
return state.answers[index] === q.correctAnswer
? score + 1 : score;
}, 0);
}
// 2. Get Wrong Answers using filter
function getWrongAnswers() {
return state.questions.filter((q, index) => {
return state.answers[index] !== q.correctAnswer;
});
}
// 3. Render Review using map
function renderReview() {
const score = calculateScore();
const wrong = getWrongAnswers();
reviewDiv.innerHTML = state.questions.map((q, i) => {
const userAnswer = state.answers[i] || 'Skipped';
const isCorrect = userAnswer === q.correctAnswer;
return '' +
'Q' + (i+1) + ': ' + q.text + '
' +
'Your Answer: ' + userAnswer + '
' +
(!isCorrect ? 'Correct: ' + q.correctAnswer + '
' : '') +
'';
}).join('');
}
0 at the end is the initial accumulator value. Without it, reduce uses the first element (an object), which would break the addition.setInterval runs a function every X milliseconds. Perfect for countdown timers in quiz apps.
CRITICAL: You MUST store the interval ID and call clearInterval when the quiz ends or component unmounts. Otherwise — memory leak!
If the user clicks "Next", reset the timer. If the timer hits 0, auto-submit the current question.
Timer logic must interact safely with the state machine — never start a timer if status !== 'playing'.
let timerId = null;
function startTimer() {
clearInterval(timerId); // Clear any existing timer
state.timeLeft = 30;
renderTimer();
timerId = setInterval(() => {
state.timeLeft--;
renderTimer();
if (state.timeLeft <= 0) {
clearInterval(timerId);
handleTimeUp();
}
}, 1000);
}
function handleTimeUp() {
if (state.currentIndex < state.questions.length - 1) {
state.currentIndex++;
startTimer(); // Restart for next question
} else {
finishQuiz();
}
render();
}
function finishQuiz() {
clearInterval(timerId); // MUST clear on finish!
state.status = 'finished';
render();
}
setInterval(fn, 1000) does NOT guarantee exactly 1000ms. If the main thread is busy (heavy DOM updates), callbacks queue up and fire late. For production apps, use Date.now() to measure actual elapsed time instead of counting down with timeLeft--. The countdown approach drifts; the absolute time approach stays accurate.Quiz options are dynamic — they change with every question. Use Event Delegation on the options container instead of attaching listeners to each button.
When an option is clicked, save the answer to state.answers and visually highlight it.
Use data-* attributes to map DOM clicks back to array indices — no closures needed.
const optionsContainer = document.getElementById('options');
optionsContainer.addEventListener('click', (e) => {
if (state.status !== 'playing') return; // Guard clause
const option = e.target.closest('.option');
if (!option) return; // Click wasn't on an option
const questionIndex = parseInt(option.dataset.qIndex);
const answerValue = option.dataset.value;
// Save answer to state
state.answers[questionIndex] = answerValue;
// Visual feedback
optionsContainer.querySelectorAll('.option').forEach(opt => {
opt.classList.remove('selected');
});
option.classList.add('selected');
});
// HTML Generation with data attributes
function renderQuestion() {
const q = state.questions[state.currentIndex];
optionsContainer.innerHTML = q.options.map(opt =>
''
).join('');
}
Quiz apps need to track high scores across sessions using localStorage.
Load high scores on init. Update them only if the new score qualifies for the list.
Keep the array sorted descending and limit to top 5 scores to prevent localStorage from growing indefinitely.
function saveHighScore(score) {
const highScores = loadHighScores();
highScores.push({ score, date: new Date().toISOString() });
// Sort descending by score
highScores.sort((a, b) => b.score - a.score);
// Keep only top 5 scores
const top5 = highScores.slice(0, 5);
localStorage.setItem('quiz_high_scores', JSON.stringify(top5));
}
function loadHighScores() {
try {
const data = localStorage.getItem('quiz_high_scores');
return data ? JSON.parse(data) : [];
} catch {
return []; // Corrupted data? Start fresh.
}
}
QuotaExceededError on write. Prevention is better than debugging.Lo kar liya — Key Points:
- ✅ Model quiz logic as a State Machine with explicit statuses ('start', 'playing', 'finished') to prevent invalid actions
- ✅ Use reduce for score calculation, filter for extracting wrong answers, and map for rendering review lists
- ✅ Always store setInterval IDs and call clearInterval on quiz end or component unmount to prevent memory leaks
- ✅ Use Event Delegation on the options container to handle dynamic answer buttons efficiently
- ✅ Save and sort high scores in localStorage, capping the list size to avoid exceeding storage limits
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login