Introduction
When I started building my quiz application, like many developers, I began with a single JavaScript file that grew larger and more complex over time. As the application evolved with new features like theme switching, timers, and hint systems, maintaining this monolithic structure became increasingly challenging.
In this article, I'll walk you through how I transformed a 1000+ line JavaScript file into a clean, modular architecture using ES6 modules, improving maintainability and setting the stage for future enhancements.
The Problem: Monolithic JavaScript
My original application had all functionality in a single script.js
file:
- Quiz loading and rendering
- User interaction handling
- Theme management
- Timer functionality
- Score tracking
- Navigation between sections
This approach led to several issues:
- Difficulty finding specific code
- Function name collisions
- Unclear dependencies between components
- Challenges when adding new features
- Testing complications
The Solution: ES6 Modules
I decided to refactor the application using ES6 modules, which provide several benefits:
- Encapsulation: Each module manages its own functionality
- Explicit dependencies: Clear imports/exports show relationships
- Reduced global scope pollution: Variables stay within their modules
- Better organization: Logical grouping of related code
- Improved maintainability: Smaller, focused files
Project Structure
I reorganized the codebase into the following structure:
quiz-app/
├── index.html
├── css/
│ └── styles.css
├── js/
│ ├── main.js # Application entry point
│ ├── data/ # Data management
│ │ ├── sample-quiz.js # Sample quiz data
│ │ └── score-manager.js # Score history management
│ ├── quiz/ # Quiz logic
│ │ ├── quiz-engine.js # Core quiz functionality
│ │ └── quiz-loader.js # Quiz loading and filtering
│ ├── ui/ # User interface
│ │ ├── navigation.js # Navigation between sections
│ │ └── theme-manager.js # Theme management
│ └── utils/ # Utilities
│ └── timer.js # Timer functionality
└── quizzes/ # JSON quiz files
The Refactoring Process
Step 1: Identify Core Components
I began by identifying distinct functional areas in my application:
- Quiz data management
- Quiz engine (question rendering, answer checking)
- UI components (navigation, theme)
- Utilities (timer, score tracking)
Step 2: Create the Entry Point
I created a clean main.js
file as the application's entry point:
// Main Application Entry Point
import { initTheme } from './ui/theme-manager.js';
import { setupNavigation } from './ui/navigation.js';
import { initQuizzes } from './quiz/quiz-loader.js';
document.addEventListener('DOMContentLoaded', async function() {
console.log('Quiz Application initializing...');
// Initialize theme
initTheme();
// Configure navigation
setupNavigation();
// Load quizzes
await initQuizzes();
console.log('Quiz Application initialized successfully');
});
This simplified entry point makes the application's initialization process clear and concise.
Step 3: Extract Theme Management
The theme management functionality was extracted into its own module:
// Theme management module
export function initTheme() {
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.querySelector('.theme-icon');
// Check for saved theme preference or use default
const savedTheme = localStorage.getItem('quizTheme') || 'light';
// Apply the saved theme
document.documentElement.setAttribute('data-theme', savedTheme);
// Update the theme icon
updateThemeIcon(savedTheme);
// Add event listener for theme toggle
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
// Update theme with animation
applyThemeWithAnimation(newTheme);
// Save preference
localStorage.setItem('quizTheme', newTheme);
});
}
// Helper functions...
Step 4: Create the Quiz Engine
The core quiz functionality was moved to a dedicated module:
// Quiz engine module
import { availableQuizzes } from './quiz-loader.js';
import { hideAllSections } from '../ui/navigation.js';
import { startTimer, stopTimer } from '../utils/timer.js';
import { saveScoreToHistory } from '../data/score-manager.js';
// Quiz state
let currentQuiz = null;
let currentQuestionIndex = 0;
let score = 0;
let selectedOption = null;
let questionAnswered = false;
// DOM Elements
const quizTitle = document.getElementById('quiz-title');
// Other elements...
// Start a quiz
export function startQuiz(quizId) {
const quiz = availableQuizzes.find(q => q.id === quizId);
if (!quiz) {
console.error(`Quiz with ID ${quizId} not found!`);
return;
}
// Initialize quiz state
currentQuiz = quiz.data;
currentQuestionIndex = 0;
score = 0;
// Set up UI
hideAllSections();
const quizSection = document.getElementById('quiz-section');
quizSection.classList.remove('hidden');
quizSection.classList.add('active');
// Start timer
startTimer();
// Load first question
loadQuestion();
}
// Other functions...
Step 5: Update HTML to Use Modules
The final step was updating the HTML file to use the new module system:
<!-- Replace the old script tag -->
<script src="js/main.js" type="module"></script>
Key Improvements
1. State Management
Each module now manages its own state, reducing global variables and potential conflicts:
// In quiz-engine.js - State is module-scoped
let currentQuiz = null;
let currentQuestionIndex = 0;
let score = 0;
// In timer.js - Timer state is encapsulated
let timerInterval = null;
let quizStartTime = 0;
2. Clear Dependencies
Dependencies between modules are now explicit through imports:
// In quiz-engine.js
import { startTimer, stopTimer } from '../utils/timer.js';
import { saveScoreToHistory } from '../data/score-manager.js';
3. Simplified Testing
With modules, testing becomes more straightforward as each component can be tested in isolation.
4. Enhanced Maintainability
Adding new features is now easier. For example, if I want to add a new quiz category filter, I only need to modify the relevant module without worrying about breaking unrelated functionality.
Challenges Faced
Browser Compatibility
ES6 modules require proper CORS handling, which means the application needs to be served from a web server rather than opened directly from the file system. I solved this by using a simple local server during development:
php -S localhost:8000
Module Path Resolution
Relative paths in module imports required careful attention, especially when refactoring deeper module hierarchies:
// Correct relative path
import { startTimer } from '../utils/timer.js';
// Not using the correct path would cause errors
Results
The refactoring significantly improved the codebase:
- Code size reduction: Individual files are now 50-250 lines instead of 1000+
- Improved readability: Each file has a clear, single responsibility
- Better performance: Only necessary code is loaded when needed
- Enhanced developer experience: Finding and modifying code is much faster
Conclusion
Refactoring a monolithic JavaScript application into a modular architecture using ES6 modules was well worth the effort. The codebase is now more maintainable, scalable, and follows modern JavaScript best practices.
If you're facing similar challenges with a growing JavaScript application, I highly recommend taking the modular approach. Start by identifying natural boundaries between components, extract them into separate modules, and establish clear interfaces between them.
The initial investment in refactoring will pay dividends in the form of easier maintenance, faster feature development, and a more enjoyable development experience.
Top comments (0)