Building a Quiz Component in React with QuizAPI
Build a reusable React quiz component that fetches questions from QuizAPI, manages quiz state, and displays scores. Full TypeScript implementation included.
What You'll Build
A self-contained React quiz component that fetches questions from QuizAPI, displays them one at a time with a progress bar, and shows the final score. It's fully typed with TypeScript and uses only React hooks for state management.
Prerequisites
- Node.js 20+
- React 18+ project (Create React App, Vite, or Next.js)
- A QuizAPI API key (free at quizapi.io)
- Basic React and TypeScript knowledge
Setup
If you don't have a React project yet:
npm create vite@latest quiz-app -- --template react-ts cd quiz-app npm install
Add your API key to .env:
VITE_QUIZAPI_KEY=your-api-key-here VITE_QUIZAPI_URL=https://quizapi.io/api/v1
Types
Define the data shapes returned by QuizAPI:
1// src/types/quiz.ts 2export interface Answer { 3 id: string; 4 text: string; 5 isCorrect: boolean; 6} 7 8export interface Question { 9 id: string; 10 text: string; 11 explanation: string | null; 12 answers: Answer[]; 13} 14 15export interface Quiz { 16 id: string; 17 title: string; 18 description: string; 19 category: string; 20 difficulty: string; 21 questionCount: number; 22}
API Client
Create a lightweight client for QuizAPI:
1// src/lib/api.ts 2const API_URL = import.meta.env.VITE_QUIZAPI_URL; 3const API_KEY = import.meta.env.VITE_QUIZAPI_KEY; 4 5const headers = { Authorization: `Bearer ${API_KEY}` }; 6 7export async function fetchQuizzes(limit = 10): Promise<Quiz[]> { 8 const res = await fetch(`${API_URL}/quizzes?limit=${limit}`, { headers }); 9 const json = await res.json(); 10 return json.data; 11} 12 13export async function fetchQuestions(quizId: string): Promise<Question[]> { 14 const res = await fetch(`${API_URL}/questions?quizId=${quizId}`, { headers }); 15 const json = await res.json(); 16 return json.data; 17}
The Quiz Component
This is the core component. It handles three phases: intro, playing, and results.
1// src/components/QuizPlayer.tsx 2import { useState, useEffect, useCallback } from "react"; 3import type { Question, Answer } from "../types/quiz"; 4import { fetchQuestions } from "../lib/api"; 5 6interface QuizPlayerProps { 7 quizId: string; 8 quizTitle: string; 9 onComplete?: (score: number, total: number) => void; 10} 11 12export function QuizPlayer({ quizId, quizTitle, onComplete }: QuizPlayerProps) { 13 const [questions, setQuestions] = useState<Question[]>([]); 14 const [current, setCurrent] = useState(0); 15 const [score, setScore] = useState(0); 16 const [selected, setSelected] = useState<string | null>(null); 17 const [showFeedback, setShowFeedback] = useState(false); 18 const [phase, setPhase] = useState<"loading" | "playing" | "results">("loading"); 19 20 useEffect(() => { 21 fetchQuestions(quizId).then((data) => { 22 setQuestions(data); 23 setPhase("playing"); 24 }); 25 }, [quizId]); 26 27 const question = questions[current]; 28 const total = questions.length; 29 const progress = total > 0 ? ((current + 1) / total) * 100 : 0; 30 31 const handleAnswer = useCallback( 32 (answerId: string) => { 33 if (showFeedback) return; 34 setSelected(answerId); 35 setShowFeedback(true); 36 37 const isCorrect = question.answers.find( 38 (a) => a.id === answerId 39 )?.isCorrect; 40 if (isCorrect) setScore((s) => s + 1); 41 42 // Auto-advance after 1.5s 43 setTimeout(() => { 44 if (current + 1 < total) { 45 setCurrent((c) => c + 1); 46 setSelected(null); 47 setShowFeedback(false); 48 } else { 49 const finalScore = isCorrect ? score + 1 : score; 50 setPhase("results"); 51 onComplete?.(finalScore, total); 52 } 53 }, 1500); 54 }, 55 [current, total, score, showFeedback, question, onComplete] 56 ); 57 58 if (phase === "loading") { 59 return <div className="quiz-loading">Loading questions...</div>; 60 } 61 62 if (phase === "results") { 63 const percentage = Math.round((score / total) * 100); 64 return ( 65 <div className="quiz-results"> 66 <h2>Quiz Complete!</h2> 67 <div className="score-display"> 68 <span className="score-number">{percentage}%</span> 69 <span className="score-detail"> 70 {score} out of {total} correct 71 </span> 72 </div> 73 <button 74 onClick={() => { 75 setCurrent(0); 76 setScore(0); 77 setSelected(null); 78 setShowFeedback(false); 79 setPhase("playing"); 80 }} 81 > 82 Try Again 83 </button> 84 </div> 85 ); 86 } 87 88 return ( 89 <div className="quiz-player"> 90 <div className="quiz-header"> 91 <h2>{quizTitle}</h2> 92 <span> 93 Question {current + 1} of {total} 94 </span> 95 </div> 96 97 {/* Progress bar */} 98 <div className="progress-bar"> 99 <div className="progress-fill" style={{ width: `${progress}%` }} /> 100 </div> 101 102 {/* Question */} 103 <div className="question"> 104 <h3>{question.text}</h3> 105 <div className="answers"> 106 {question.answers.map((answer) => { 107 let className = "answer-btn"; 108 if (showFeedback && answer.id === selected) { 109 className += answer.isCorrect ? " correct" : " incorrect"; 110 } else if (showFeedback && answer.isCorrect) { 111 className += " correct"; 112 } 113 return ( 114 <button 115 key={answer.id} 116 className={className} 117 onClick={() => handleAnswer(answer.id)} 118 disabled={showFeedback} 119 > 120 {answer.text} 121 </button> 122 ); 123 })} 124 </div> 125 </div> 126 127 {/* Explanation */} 128 {showFeedback && question.explanation && ( 129 <div className="explanation"> 130 <strong>Explanation:</strong> {question.explanation} 131 </div> 132 )} 133 </div> 134 ); 135}
Using the Component
1// src/App.tsx 2import { useState, useEffect } from "react"; 3import { QuizPlayer } from "./components/QuizPlayer"; 4import { fetchQuizzes } from "./lib/api"; 5import type { Quiz } from "./types/quiz"; 6 7function App() { 8 const [quizzes, setQuizzes] = useState<Quiz[]>([]); 9 const [activeQuiz, setActiveQuiz] = useState<Quiz | null>(null); 10 11 useEffect(() => { 12 fetchQuizzes(10).then(setQuizzes); 13 }, []); 14 15 if (activeQuiz) { 16 return ( 17 <div className="app"> 18 <button onClick={() => setActiveQuiz(null)}>Back to list</button> 19 <QuizPlayer 20 quizId={activeQuiz.id} 21 quizTitle={activeQuiz.title} 22 onComplete={(score, total) => { 23 console.log(`Scored ${score}/${total}`); 24 }} 25 /> 26 </div> 27 ); 28 } 29 30 return ( 31 <div className="app"> 32 <h1>Quiz App</h1> 33 <div className="quiz-list"> 34 {quizzes.map((quiz) => ( 35 <div key={quiz.id} className="quiz-card"> 36 <h2>{quiz.title}</h2> 37 <p>{quiz.description}</p> 38 <div className="quiz-meta"> 39 <span>{quiz.difficulty}</span> 40 <span>{quiz.questionCount} questions</span> 41 </div> 42 <button onClick={() => setActiveQuiz(quiz)}>Play</button> 43 </div> 44 ))} 45 </div> 46 </div> 47 ); 48} 49 50export default App;
Styling
Add some basic styles to make it look good:
1/* src/styles/quiz.css */ 2.quiz-player { 3 max-width: 640px; 4 margin: 0 auto; 5 padding: 2rem; 6} 7 8.progress-bar { 9 height: 4px; 10 background: #e5e7eb; 11 border-radius: 2px; 12 margin: 1rem 0; 13} 14 15.progress-fill { 16 height: 100%; 17 background: #3b82f6; 18 border-radius: 2px; 19 transition: width 0.3s ease; 20} 21 22.answer-btn { 23 display: block; 24 width: 100%; 25 padding: 0.75rem 1rem; 26 margin: 0.5rem 0; 27 border: 2px solid #e5e7eb; 28 border-radius: 8px; 29 background: white; 30 cursor: pointer; 31 text-align: left; 32 font-size: 1rem; 33 transition: border-color 0.2s; 34} 35 36.answer-btn:hover:not(:disabled) { 37 border-color: #3b82f6; 38} 39 40.answer-btn.correct { 41 border-color: #10b981; 42 background: #ecfdf5; 43} 44 45.answer-btn.incorrect { 46 border-color: #ef4444; 47 background: #fef2f2; 48} 49 50.score-number { 51 font-size: 3rem; 52 font-weight: bold; 53 color: #3b82f6; 54}
Next Steps
- Add a timer for each question
- Save results to localStorage for offline history
- Add category filtering to the quiz list
- Implement the QuizAPI leaderboard to show high scores
- Use the embed endpoint as an alternative to building from scratch
Resources
Think you understand Tutorial? Put your skills to the test with hands-on quiz questions.
Enjoyed this article?
Share it with your team or try our quiz platform.
Stay Updated
Get the latest tutorials and API tips delivered to your inbox.
No spam, unsubscribe anytime.
Related Articles
How to Build a Quiz App with Django and QuizAPI
Step-by-step guide to building a quiz application with Django using the QuizAPI REST API. Fetch questions, render a quiz UI, and submit scores.
Building a Quiz Leaderboard with Real-Time Updates
Build a live quiz leaderboard with ranking algorithms, efficient data models, and real-time delivery using SSE and WebSockets.
Add Quizzes to Your Laravel App with QuizAPI
Integrate QuizAPI into a Laravel application with the HTTP client, Blade templates, and proper form handling for a complete quiz experience.