Web Development12 min read

Three Years Later, I Finally Submitted My Frontend Mentor...

I started this project in early 2021. It lived in a local repository, gathered dust, and slowly accumulated technical debt as browser APIs changed and my...

Listen to Article

Click play to listen to audio narration

Introduction

I started this project in early 2021. It lived in a local repository, gathered dust, and slowly accumulated technical debt as browser APIs changed and my own standards evolved. Three years of context switching, framework churn, and unbounded scope creep turned a simple UI exercise into a maintenance trap. Last month, I audited the codebase, stripped the dead weight, and finally pushed the submission commit.

This isn’t a tutorial on building a dashboard widget. It is a postmortem on why frontend projects stall and how architectural discipline brings them back to life. When you leave code dormant for years, you inherit a different set of problems: deprecated patterns, unhandled edge cases, and a tangled dependency graph that resists incremental updates. The path to submission required treating a personal project with the same rigor as a production service.

Why This Matters

Frontend architecture decays faster than backend infrastructure. Browsers ship new rendering engines, accessibility standards tighten, and Core Web Vitals shift from metrics to hard requirements. Engineers frequently treat personal projects as disposable prototypes, but shipping them demands production-grade discipline. Understanding how to refactor legacy DOM manipulation into a maintainable state machine saves thousands of hours in enterprise codebases.

The real pain point isn’t writing the initial markup. It is managing state transitions, preventing layout thrashing, and ensuring deterministic rendering across browsers. When you build systems that outlive their initial sprint, you learn that architecture is not about choosing the newest framework. It is about isolating side effects, versioning your data flow, and building components that survive refactoring without breaking.

How It Works

The refactored system decouples user input from rendering. Events normalize into structured actions before reaching the state layer. A deterministic reducer computes the next state snapshot. A subscription layer triggers surgical DOM updates instead of full re-renders. This unidirectional flow eliminates race conditions and makes debugging traceable.

flowchart TD
    User[User Interaction] --> Parser[Event Normalizer]
    Parser --> Bus[Central Event Bus]
    Bus --> Reducer[State Reducer]
    Reducer --> Store[Immutable Store]
    Store --> Subscribers[View Subscribers]
    Subscribers --> DOM[Targeted DOM Patch]
    DOM --> Browser[Browser Render Cycle]
    Reducer -.->|Metrics| Observer[Performance Observer]

The workflow operates in four distinct phases:

  1. Event Normalization: Raw DOM events strip browser-specific properties and flatten into a consistent action payload. This layer handles debouncing, throttling, and accessibility keyboard mapping.
  2. State Reduction: Actions enter a pure reducer function. The reducer reads the current state snapshot, applies the transformation, and returns a new reference. No direct mutation occurs.
  3. Subscription Dispatch: Listeners receive the new state reference. Each subscriber checks a lightweight equality guard before committing updates to prevent redundant repaints.
  4. Targeted DOM Patching: Subscribers apply changes to isolated DOM fragments. The browser batches these mutations and executes a single layout pass, preserving scroll position and focus state.

This architecture removes the guesswork from UI updates. When a bug surfaces, you trace the action through the reducer, verify the state transition, and isolate the subscriber responsible for the DOM mutation.

Core Concepts

The system relies on four foundational principles that separate hobby code from engineering-grade frontends:

  • Unidirectional Data Flow: State moves in one direction. User input triggers actions, actions mutate state, state changes trigger rendering. Circular dependencies are architecturally impossible.
  • Immutable State Snapshots: Every state update produces a new object reference. This enables cheap equality checks, simplifies time-travel debugging, and prevents accidental shared references across components.
  • Progressive Enhancement: The core functionality works with vanilla JavaScript and semantic HTML. JavaScript enhances the experience rather than replacing it. Accessibility attributes remain in the markup, not injected dynamically after hydration.
  • Boundary Isolation: Side effects live at the edges. Data fetching, storage synchronization, and analytics fire from dedicated boundary modules. Pure UI components receive state and return rendering instructions. They never call fetch or localStorage directly.

These concepts compound over time. A project built with them refactors cleanly. A project built without them requires wholesale rewrites when requirements shift.

Examples & Code Walkthrough

The original implementation suffered from tightly coupled event handlers and global state leakage. Here is how it looked before the refactor:

// Legacy pattern: tightly coupled, side-effect heavy
window._appState = { loading: false, data: null };

document.getElementById('fetch-btn').addEventListener('click', function() {
  window._appState.loading = true;
  document.getElementById('spinner').style.display = 'block';
  
  fetch('/api/v1/widgets')
    .then(res => res.json())
    .then(payload => {
      window._appState.data = payload;
      renderTable(payload); // Direct DOM mutation
      document.getElementById('spinner').style.display = 'none';
    })
    .catch(err => console.error('Fetch failed', err));
});

This approach blocks the main thread during heavy renders, pollutes the global scope, and makes testing nearly impossible. The refactored version introduces a deterministic store with structured action handling:

class Store {
  #state;
  #listeners = new Set();

  constructor(initialState) {
    this.#state = structuredClone(initialState);
  }

  subscribe(fn) {
    this.#listeners.add(fn);
    return () => this.#listeners.delete(fn);
  }

  dispatch(action) {
    const nextState = this.#reduce(this.#state, action);
    if (nextState !== this.#state) {
      this.#state = nextState;
      this.#listeners.forEach(cb => cb(this.#state));
    }
  }

  #reduce(state, action) {
    switch (action.type) {
      case 'FETCH_INIT':
        return { ...state, loading: true, error: null };
      case 'FETCH_SUCCESS':
        return { ...state, loading: false, data: action.payload };
      case 'FETCH_FAILURE':
        return { ...state, loading: false, error: action.error };
      default:
        return state;
    }
  }
}

The store isolates state transitions. structuredClone guarantees deep immutability without external dependencies. The subscribe method returns an unsubscribe function, preventing memory leaks when components mount and unmount.

Accessibility requires explicit focus management. Dynamic content injection breaks keyboard navigation unless you trap focus intentionally:

function attachFocusTrap(container) {
  const focusable = container.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  return (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  };
}

This utility ensures keyboard users cycle through interactive elements without escaping the modal or widget boundary. It attaches to the container’s keydown listener and detaches when the component unmounts

Tags:#later#years#web development#three
L

Written by Lead Frontend & Web Architect

Editorial staff persona leading coverage on modern web architectures, state management, web performance optimization, and client-side framework engineering.

View Profile
Recommended For You

Related Articles

Quick:
Navigate Select
Loading search index...