Skip to main content

<Quiz/>

The Quiz component is a comprehensive container component for creating interactive quizzes in Pico LMS. It provides complete quiz lifecycle management including question navigation, shuffling, scoring, progress tracking, and result display.

Overview​

Quiz is a top-level component that orchestrates the entire quiz experience. It handles:

  • Question Management: Sequential presentation and navigation
  • Shuffling: Deterministic question randomization using Fisher-Yates algorithm
  • State Management: Controlled quiz state with auto-save capabilities
  • Scoring: Automatic score calculation with pass/fail thresholds
  • Progress Tracking: Real-time progress updates and callbacks
  • Results Display: Comprehensive score and performance summaries
  • Storage: Optional persistence with custom storage managers

Import​

import { Quiz } from '@scinforma/picolms';

Basic Usage​

Simple Quiz​

<Quiz
config={{
id: 'intro-quiz',
title: 'Introduction to Biology',
questions: [
{
id: 'q1',
type: 'multiple-choice',
question: 'What is photosynthesis?',
points: 10,
},
{
id: 'q2',
type: 'short-answer',
question: 'Describe cellular respiration.',
points: 15,
},
],
showScore: true,
}}
/>

Quiz with Passing Score​

<Quiz
config={{
id: 'certification-quiz',
title: 'Certification Exam',
description: 'Complete all questions to earn your certificate',
questions: questions,
shuffleQuestions: true,
showScore: true,
passingScore: 75,
}}
onQuizSubmit={(result) => {
console.log('Quiz completed:', result);
}}
/>

Props​

Required Props​

config​

  • Type: QuizConfig
  • Description: Configuration object defining the quiz structure, behavior, and scoring rules.
config={{
id: 'quiz-1',
title: 'My Quiz',
questions: [...],
showScore: true,
passingScore: 70,
}}

Optional Props​

children​

  • Type: ReactNode
  • Description: Custom content to render within the quiz container. Has access to quiz context.
<Quiz config={config}>
<CustomProgressBar />
<CustomNavigationButtons />
</Quiz>

onAnswerChange​

  • Type: (questionId: string, answer: QuestionAnswer) => void
  • Description: Callback fired when any question's answer changes.
onAnswerChange={(questionId, answer) => {
console.log(`Question ${questionId} answered:`, answer.value);
logActivity(questionId, answer);
}}

onQuestionSubmit​

  • Type: (submission: QuestionSubmission) => void
  • Description: Callback fired when an individual question is submitted.
onQuestionSubmit={(submission) => {
console.log('Question submitted:', submission);
saveToDatabase(submission);
}}

onQuizSubmit​

  • Type: (result: QuizResult) => void
  • Description: Callback fired when the entire quiz is completed and submitted.
onQuizSubmit={(result) => {
const passed = (result.score / result.maxScore) * 100 >= 75;
if (passed) {
awardCertificate(result);
}
}}

onProgressChange​

  • Type: (progress: any) => void
  • Description: Callback fired when quiz progress updates (questions answered, time spent, etc.).
onProgressChange={(progress) => {
updateProgressBar(progress);
sendAnalytics(progress);
}}

renderQuestion​

  • Type: (question: any, index: number) => ReactNode
  • Description: Custom renderer function for individual questions. Overrides default question display.
renderQuestion={(question, index) => (
<div className="custom-question">
<h3>Question {index + 1}</h3>
<QuestionComponent question={question} />
</div>
)}

loadedResult​

  • Type: LoadedQuizResult
  • Description: Pre-existing quiz result for resuming or reviewing a previous attempt.
const previousAttempt = loadFromStorage('quiz-123');

<Quiz
config={config}
loadedResult={previousAttempt}
/>

storageManager​

  • Type: QuizStorageManager
  • Description: Custom storage manager instance for persisting quiz state and progress.
import { QuizStorageManager } from '@scinforma/picolms';

const storage = new QuizStorageManager('local');

<Quiz
config={config}
storageManager={storage}
/>

autoSaveInterval​

  • Type: number
  • Description: Interval in milliseconds for automatic saving of quiz progress. Requires storageManager.
<Quiz
config={config}
storageManager={storage}
autoSaveInterval={5000} // Save every 5 seconds
/>

className​

  • Type: string
  • Description: Additional CSS class names to apply to the quiz container.
<Quiz
config={config}
className="custom-quiz-theme"
/>

QuizConfig Object​

The config prop accepts a QuizConfig object with the following properties:

Core Properties​

PropertyTypeRequiredDescription
idstringYesUnique identifier for the quiz (used as seed for shuffling)
titlestringYesQuiz title displayed in the header
descriptionstringNoOptional description shown below the title
instructionsReactNodeNoOptional instructions displayed before quiz starts
questionsQuestion[]YesArray of question configuration objects
shuffleQuestionsbooleanNoEnable deterministic question randomization (default: false)
showScorebooleanNoDisplay score after quiz completion (default: false)
passingScorenumberNoPassing percentage threshold (0-100)

Example Configuration​

const config: QuizConfig = {
id: 'advanced-quiz-001',
title: 'Advanced JavaScript Concepts',
description: 'Test your knowledge of modern JavaScript',
instructions: (
<div>
<p>This quiz consists of 10 questions.</p>
<p>You must score 80% or higher to pass.</p>
<p>Good luck!</p>
</div>
),
questions: [
{
id: 'q1',
type: 'multiple-choice',
question: 'What is a closure?',
options: ['A', 'B', 'C', 'D'],
correctAnswer: 'A',
points: 10,
},
// ... more questions
],
shuffleQuestions: true,
showScore: true,
passingScore: 80,
};

QuizResult Object​

The result object passed to onQuizSubmit has the following structure:

interface QuizResult {
score: number; // Total points earned
maxScore: number; // Maximum possible points
status: 'submitted' | 'graded'; // Quiz completion status
answers: Record<string, QuestionAnswer>; // Map of question IDs to answers
}

LoadedQuizResult Object​

For resuming or reviewing previous attempts:

interface LoadedQuizResult {
score: number;
maxScore: number;
answers: Record<string, QuestionAnswer>;
status: 'submitted' | 'graded';
// Additional result metadata
}

Question Shuffling Algorithm​

The Quiz component uses the Fisher-Yates shuffle algorithm with deterministic seeding to randomize question order while maintaining consistency.

How It Works​

  1. Quiz ID is converted to a numeric seed
  2. A seeded random number generator ensures the same shuffle order for the same quiz ID
  3. Questions are shuffled once when the component mounts and memoized to prevent re-shuffling on re-renders

Example​

// Same quiz ID always produces the same question order
<Quiz
config={{
id: 'quiz-123', // This ID generates a consistent seed
shuffleQuestions: true,
questions: [q1, q2, q3, q4, q5],
}}
/>
// Questions might appear as: q3, q1, q5, q2, q4 (consistent across sessions)

Implementation Details​

/**
* Fisher-Yates shuffle with seeded randomization
* Seed is generated from quiz ID for consistency
*/
function shuffleArray<T>(array: T[], seed?: number): T[] {
const shuffled = [...array];
const random = seed ? seededRandom(seed) : Math.random;

for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}

return shuffled;
}

Context​

Quiz provides a QuizContext that child components can access using the useQuizContext hook:

import { useQuizContext } from '@scinforma/picolms';

function CustomQuizControls() {
const {
state,
currentQuestion,
showingResults,
goToNextQuestion,
goToPreviousQuestion,
submitQuiz,
} = useQuizContext();

return (
<div className="quiz-controls">
<p>Question {state.currentQuestionIndex + 1} of {state.totalQuestions}</p>
<p>Score: {state.score} / {state.maxScore}</p>
<button onClick={goToPreviousQuestion} disabled={state.currentQuestionIndex === 0}>
Previous
</button>
<button onClick={goToNextQuestion}>
Next
</button>
</div>
);
}

<Quiz config={config}>
<CustomQuizControls />
</Quiz>

Context Values​

PropertyTypeDescription
stateQuizStateCurrent quiz state (score, status, answers, etc.)
currentQuestionQuestionCurrently displayed question object
showingResultsbooleanWhether results screen is displayed
goToNextQuestion() => voidNavigate to next question
goToPreviousQuestion() => voidNavigate to previous question
submitQuiz() => voidSubmit the entire quiz

Quiz Status Flow​

The quiz progresses through the following states:

  1. Initial/In Progress: User is answering questions
  2. Submitted: All questions answered and quiz submitted
  3. Graded: Quiz has been graded (if applicable)
// Status is automatically managed by the component
state.status === 'initial' // Quiz in progress
state.status === 'submitted' // Quiz completed
state.status === 'graded' // Quiz graded (if grading is implemented)

CSS Classes​

The component uses these CSS classes with the picolms- prefix:

/* Main container */
.picolms-quiz-container { }

/* Header section */
.picolms-quiz-header { }
.picolms-quiz-title { }
.picolms-quiz-description { }
.picolms-quiz-instructions { }

/* Content area */
.picolms-quiz-content { }
.picolms-quiz-question-container { }
.picolms-quiz-question-number { }

/* Results display */
.picolms-quiz-results-summary { }
.picolms-results-heading { }
.picolms-results-score { }
.picolms-results-percentage { }
.picolms-results-status { }
.picolms-results-passed { }
.picolms-results-failed { }

Custom Styling​

<Quiz
config={config}
className="dark-theme"
/>
.dark-theme .picolms-quiz-container {
background-color: #1a1a1a;
color: #ffffff;
}

Examples​

Quiz with All Features​

import { Quiz, QuizStorageManager } from '@scinforma/picolms';

const storageManager = new QuizStorageManager('local');

function ComprehensiveQuiz() {
const handleQuizComplete = (result: QuizResult) => {
const percentage = (result.score / result.maxScore) * 100;
const passed = percentage >= 75;

// Save to backend
fetch('/api/quiz-results', {
method: 'POST',
body: JSON.stringify({
...result,
passed,
percentage,
}),
});

// Show notification
if (passed) {
showSuccessMessage('Congratulations! You passed!');
} else {
showErrorMessage('Keep studying and try again!');
}
};

return (
<Quiz
config={{
id: 'comprehensive-quiz',
title: 'Final Examination',
description: 'This exam covers all course material',
instructions: (
<ul>
<li>Answer all questions carefully</li>
<li>You have unlimited time</li>
<li>Passing score is 75%</li>
</ul>
),
questions: courseQuestions,
shuffleQuestions: true,
showScore: true,
passingScore: 75,
}}
storageManager={storageManager}
autoSaveInterval={10000}
onAnswerChange={(qId, answer) => {
console.log('Answer updated:', qId, answer);
}}
onQuizSubmit={handleQuizComplete}
className="final-exam"
>
<ProgressTracker />
<NavigationButtons />
</Quiz>
);
}

Quiz with Custom Question Renderer​

import ReactMarkdown from 'react-markdown';
import { MultipleChoiceQuestion, ShortAnswerQuestion } from '@scinforma/picolms';

function CustomRenderedQuiz() {
const renderQuestion = (question: any, index: number) => {
return (
<div className="question-wrapper">
<div className="question-header">
<span className="question-number">Question {index + 1}</span>
<span className="question-points">{question.points} points</span>
</div>

<div className="question-text">
<ReactMarkdown>{question.question}</ReactMarkdown>
</div>

{question.type === 'multiple-choice' && (
<MultipleChoiceQuestion question={question} />
)}

{question.type === 'short-answer' && (
<ShortAnswerQuestion question={question} />
)}
</div>
);
};

return (
<Quiz
config={config}
renderQuestion={renderQuestion}
/>
);
}

Resume Previous Attempt​

import { useState, useEffect } from 'react';

function ResumableQuiz() {
const [loadedResult, setLoadedResult] = useState<LoadedQuizResult | undefined>();
const [loading, setLoading] = useState(true);

useEffect(() => {
// Load previous attempt from storage or API
const loadPreviousAttempt = async () => {
try {
const saved = await fetch('/api/quiz/in-progress/quiz-123').then(r => r.json());
setLoadedResult(saved);
} catch (error) {
console.error('Failed to load previous attempt:', error);
} finally {
setLoading(false);
}
};

loadPreviousAttempt();
}, []);

if (loading) {
return <div>Loading quiz...</div>;
}

return (
<Quiz
config={config}
loadedResult={loadedResult}
onQuizSubmit={(result) => {
// Clear saved progress
fetch('/api/quiz/in-progress/quiz-123', { method: 'DELETE' });
// Save final result
fetch('/api/quiz/results', {
method: 'POST',
body: JSON.stringify(result),
});
}}
/>
);
}

Quiz with Progress Tracking​

function TrackedQuiz() {
const [progress, setProgress] = useState({
answered: 0,
total: 0,
timeSpent: 0,
});

return (
<>
<div className="progress-bar">
<div className="progress-info">
{progress.answered} of {progress.total} answered
</div>
<div
className="progress-fill"
style={{ width: `${(progress.answered / progress.total) * 100}%` }}
/>
</div>

<Quiz
config={config}
onProgressChange={(p) => setProgress(p)}
/>
</>
);
}

Quiz with Auto-Save and Recovery​

import { QuizStorageManager } from '@scinforma/picolms';

function AutoSavedQuiz() {
const storage = new QuizStorageManager('local');
const [showRecoveryPrompt, setShowRecoveryPrompt] = useState(false);
const [savedData, setSavedData] = useState<LoadedQuizResult | null>(null);

useEffect(() => {
// Check for saved progress
const saved = storage.load('quiz-123');
if (saved && saved.status !== 'submitted') {
setSavedData(saved);
setShowRecoveryPrompt(true);
}
}, []);

if (showRecoveryPrompt) {
return (
<div className="recovery-prompt">
<h2>Continue Previous Attempt?</h2>
<p>We found a saved quiz in progress.</p>
<button onClick={() => {
setShowRecoveryPrompt(false);
}}>
Continue
</button>
<button onClick={() => {
setSavedData(null);
storage.clear('quiz-123');
setShowRecoveryPrompt(false);
}}>
Start Fresh
</button>
</div>
);
}

return (
<Quiz
config={config}
loadedResult={savedData}
storageManager={storage}
autoSaveInterval={3000}
onQuizSubmit={() => {
storage.clear('quiz-123');
}}
/>
);
}

TypeScript​

The component is fully typed with TypeScript:

import type { QuizConfig, QuizResult, LoadedQuizResult } from '@scinforma/picolms';

const config: QuizConfig = {
id: 'typed-quiz',
title: 'TypeScript Quiz',
questions: [/* ... */],
};

const handleSubmit = (result: QuizResult): void => {
console.log('Score:', result.score);
console.log('Max Score:', result.maxScore);
console.log('Answers:', result.answers);
};

<Quiz config={config} onQuizSubmit={handleSubmit} />

Best Practices​

  1. Unique Quiz IDs: Always use unique, stable IDs for quizzes, especially when using shuffling. The ID is used as the shuffle seed.

  2. Enable Auto-Save for Long Quizzes: Use autoSaveInterval and storageManager for quizzes that may take significant time to complete.

  3. Set Appropriate Passing Scores: Consider the difficulty and importance of the quiz when setting passingScore thresholds.

  4. Provide Clear Instructions: Use the instructions field to explain quiz rules, time limits, and grading criteria.

  5. Handle Submission Events: Always implement onQuizSubmit to persist results to a backend or storage system.

  6. Use Shuffling Thoughtfully: Enable shuffleQuestions for assessments where question order shouldn't provide hints or create dependencies.

  7. Test with Loaded Results: Ensure your quiz works correctly when resuming from a loadedResult to provide a seamless user experience.

  8. Custom Rendering for Complex Content: Use renderQuestion when questions need special formatting, media, or interactive elements.

Performance Considerations​

  • Questions are shuffled once during initialization and memoized with useMemo
  • Auto-save operations are debounced to prevent excessive storage writes
  • The component re-renders only when necessary state changes occur
  • Large question sets are handled efficiently through lazy evaluation
  • BaseQuestion: Foundation component for individual questions
  • MultipleChoiceQuestion: Specialized multiple choice question component
  • ShortAnswerQuestion: Text-based answer component
  • QuizNavigation: Navigation controls for quiz progression
  • QuizResults: Results display component
  • useQuizState: Internal hook for quiz state management
  • useQuizContext: Access quiz context in child components
  • useQuizProgress: Track quiz completion progress
  • useQuizStorage: Handle quiz persistence and recovery