DEV Community

Nexus Labs
Nexus Labs

Posted on

From Spaghetti to Modules: AI's Role in My JavaScript Cleanup

If you've spent any time poking around older codebases, you know the dread. That one function. The monolith. The calculateComplexReportAndRenderDashboard() function that's 500 lines long, mutates global state, talks directly to the DOM, and has comments like // DON'T TOUCH! from five years ago. I found myself staring down one of those beasts last week, and my stomach churned.

The Monster in the Machine Room

The function was called processAnalyticsDataLegacy() and, oh boy, it lived up to its name. It ingested raw analytics data, performed about seven different aggregation steps, filtered by various criteria (which were hardcoded!), then took the final result and directly updated three different sections of a dashboard. All in one go. No parameters to speak of beyond the raw data array, and it leaned heavily on window.appConfig for everything. Testing it was a nightmare; you had to mock practically the entire browser environment. Just looking at the indentation level made my eyes water. We're talking 400+ lines of pure, unadulterated procedural JavaScript spaghetti.

My task was to add a new filtering option to this monstrosity. The thought of wading into those murky waters, trying to understand where one logical block ended and another began, felt like a multi-day quest. My hands were already sweating just thinking about the potential regressions.

My First (Futile) Attempts

My initial thought, as it often is, was 'I'll just refactor it manually.' I opened up the file, took a deep breath, and scrolled. After about ten minutes, I closed it. The mental overhead of tracking all the dependencies, figuring out what data transformations were happening where, and then trying to extract those into pure functions felt like trying to untangle a ball of yarn after a cat had its way with it. I knew it needed to be broken into smaller, testable modules – maybe processRawData, aggregateMetrics, filterByCriteria, generateDashboardViewModel, and renderDashboardComponents. But where did one end and the other begin in that tangled mess?

Then, I remembered the shiny new AI assistant I've been experimenting with. I copied the entire 400-line function and, with a sigh, pasted it into a fresh chat with ChatGPT-4. My first prompt was a hopeful, if naive, 'Refactor this function into smaller, more manageable, and testable modules.'

What I got back wasn't terrible, but it wasn't great either. The AI had done a good job of formatting the code and suggesting where export statements could go, but it hadn't truly understood the logical intent behind each massive block. It mostly just wrapped existing chunks into new functions without really clarifying the data flow or simplifying the logic. It was still a single, complex pipeline, just broken into slightly smaller, still-complex functions. No real win.

The Iterative Breakthrough

This is where the 'AHA!' moment happened: the AI isn't an oracle; it's a very fast, very patient assistant. It needs context and guidance, just like a junior developer. My mistake was asking it to solve the whole problem in one go.

I cleared the chat and started fresh, this time with Claude 3 Opus. I adopted a more iterative, conversational approach:

  1. Phase 1: Context and Overview. First, I explained the application's general purpose and what processAnalyticsDataLegacy() was supposed to achieve (processing data and updating a dashboard). Then, I pasted the entire 400-line function. I specifically asked, 'Can you help me break this processAnalyticsDataLegacy() function into several smaller, pure functions? I want each new function to have a single responsibility and be easily testable. What are the distinct logical blocks you see here, and what would be good names for functions extracted from them?'

  2. Phase 2: Step-by-Step Extraction. Claude came back with a surprisingly insightful breakdown, identifying blocks like 'Initial Data Validation and Preparation,' 'Session Aggregation,' 'Metric Calculation,' and 'DOM Updates.' Crucially, it suggested generateDashboardViewModel as a pure function and updateDashboardDOM as a separate, imperative one. This was progress!

    I then took each suggested block, one by one. 'Okay, take the part identified as 'Session Aggregation' – specifically lines 56-120 – and extract it into a new function called aggregateSessions(rawData). Make sure it returns data and doesn't have side effects.' I repeated this for each logical block.

  3. Phase 3: Recomposition and Refinement. As I extracted each piece, I'd ask Claude to show me how the original processAnalyticsDataLegacy() function would look, calling these new, smaller modules. I'd review its suggestions, point out any missed variables or incorrect data flows, and iterate. 'It looks like filterByCriteria needs access to appConfig.startDate, which is currently in the main function's scope. How would you pass that in gracefully?'

This iterative dance, a back-and-forth of prompting, reviewing, and refining, was incredibly effective. Instead of spending days manually dissecting that beast, I had a working, modular prototype within about 45 minutes of active AI prompting. The resulting code was dramatically cleaner, with functions like calculateAverageSessionDuration(sessions) and formatReportForDisplay(metrics) that were genuinely pure and easy to test.

Of course, I still had to do the final integration and human review – the AI isn't infallible and occasionally made a logical jump I had to correct. But it took the initial, paralyzing hurdle of understanding a poorly documented, complex system and completely dissolved it. It turned what felt like an impossible task into a manageable engineering exercise. The AI didn't just refactor; it helped me understand the legacy code in a structured way I couldn't achieve alone. It felt less like magic and more like having a hyper-efficient pair programmer who never got tired or frustrated.

javascript
// Before (a tiny snippet of the pain)
function processAnalyticsDataLegacy(rawData) {
let aggregated = {};
// ... 400 lines of deeply nested loops, global reads, DOM writes ...
if (window.appConfig.startDate) {
// More complex filtering logic tied to global state
}
document.getElementById('report-summary').innerText = finalResult.summary;
// ...
return finalResult;
}

// After (the vision, guided by AI)
// analytics-data-processor.js
export function aggregateRawSessions(rawData) { /* ... pure function ... / }
export function calculateMetrics(aggregatedData, config) { /
... pure function ... */ }

// dashboard-renderer.js
export function renderReportSummary(summaryData) { /* ... DOM interaction ... / }
export function updateCharts(chartData) { /
... DOM interaction ... */ }

// main-service.js
import { aggregateRawSessions, calculateMetrics } from './analytics-data-processor.js';
import { renderReportSummary, updateCharts } from './dashboard-renderer.js';

export function generateAndDisplayAnalyticsReport(rawData, appConfig) {
const aggregated = aggregateRawSessions(rawData);
const metrics = calculateMetrics(aggregated, appConfig); // Pass config explicitly!
renderReportSummary(metrics.summary);
updateCharts(metrics.chartData);
return metrics; // Return data, don't just do side effects
}

Top comments (0)