DEV Community

A0mineTV
A0mineTV

Posted on

From Monolith to Modules: Refactoring a JavaScript Quiz Application

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
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

Top comments (0)

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

Join the Runner H "AI Agent Prompting" Challenge: $10,000 in Prizes for 20 Winners!

Runner H is the AI agent you can delegate all your boring and repetitive tasks to - an autonomous agent that can use any tools you give it and complete full tasks from a single prompt.

Check out the challenge

DEV is bringing live events to the community. Dismiss if you're not interested. ❤️