DEV Community

Cover image for The Gap That LeetCode's 30 Days of JavaScript Actually Fills
Bogdan
Bogdan

Posted on

The Gap That LeetCode's 30 Days of JavaScript Actually Fills

Most coding challenges teach you to solve puzzles. LeetCode's 30 Days of JavaScript study plan does something different: it shows you how puzzle pieces can transform into bricks, ready to build real-world projects.

This distinction matters. When you solve a typical algorithmic problem, you're training your mind to think abstractly. But when you implement a debounced1 function or build an event emitter2, you're learning how real software works.

I discovered this while working through the challenges myself. The experience was less like solving brain teasers and more like archaeology - uncovering specific, modern JavaScript concepts. Each section focused on another piece of JS's modern features.

The peculiar thing about this study plan is that it won't teach you JavaScript. In fact, I believe you need to already know JavaScript reasonably well to benefit from it. What it teaches instead is how JavaScript is actually used to solve real engineering problems.

Consider the Memoize3 challenge. On the surface, it's about caching function results. But what you're really learning is why libraries like React need memoization to handle component rendering efficiently. Or take the Debounce1 problem - it's not just about implementing delays; it helps you understand, firsthand, why every modern frontend framework, elevator, and basically any system with an interactive UI, need this pattern.

This focus on practical patterns rather than language basics creates an interesting constraint; you need to be in one of two positions to benefit:

  1. You understand CS fundamentals (especially Data Structures and Algorithms) and are comfortable with JavaScript
  2. You're strong in CS theory and have some prior JavaScript exposure

Bridging CS and Software Engineering

Something odd happens between learning computer science and practicing software engineering. The transition feels like learning chess theory for years, only to find yourself playing a different game entirely - one where the rules keep changing and most moves aren't in any book.

In CS, you learn how a binary tree works. In software engineering, you spend hours debugging your API, trying to understand why the response caching isn't working. From a distance, the overlap between these worlds might seem significantly bigger than it actually is. There is a gap there, and it can often shock CS graduates when they start their careers. Unfortunately, most educational resources fail to bridge it. They either stay purely theoretical ("here's how quicksort works") or purely practical ("here's how to deploy a React app").

What makes this JavaScript study plan interesting isn't that it's particularly well-designed - it's that it creates connections between these worlds. Take the memoization problem: 2623. Memoize3. In CS terms, it's about caching computed values. But implementing it forces you to grapple with JavaScript's peculiarities around object references, function contexts, and memory management. Suddenly,
you're not just learning an algorithm - you're starting to understand why something like Redis exists.

This style repeats throughout the challenges. The Event Emitter2 implementation isn't just about a textbook observer pattern - you can look at it as the reason why taking the V8 engine out of the browser, and building Node.js around it, actually made sense. The Promise Pool4 tackles parallel execution, a.k.a., the reason why your database needs connection limiting.

The Hidden Curriculum

The sequence of problems in this study plan isn't random. It's building a mental model of modern JavaScript, layer by layer.

It starts with closures. Not because closures are the simplest concept - they're notoriously confusing - but because they're foundational to how JavaScript manages state.

function createCounter(init) {
    let count = init;
    return function() {
        return count++;
    }
}

const counter1 = createCounter(10);
console.log(counter1()); // 10
console.log(counter1()); // 11
console.log(counter1()); // 12

// const counter1 = createCounter(10);
// when this^ line executes:
// - createCounter(10) creates a new execution context
// - local variable count is initialized to 10
// - a new function is created and returned
// - this returned function maintains access 
// to the count variable in its outer scope
// - this entire bundle 
// (function (the inner one) + its access to count) 
// is what we call a closure
Enter fullscreen mode Exit fullscreen mode

This pattern is the seed of all state management in JavaScript. Once you understand how this counter works, you understand how React's useState works under the hood. You grasp why module patterns emerged in pre-ES6 JavaScript.

Then the plan moves to function transformations. These teach you function decoration - where functions wrap other functions to modify their behavior. This isn't just a technical trick; it's how Express middlewares work, how React higher-order components operate,
and also how TypeScript decorators work.

By the time you reach the asynchronous challenges, you're not just learning about Promises - you're discovering why JavaScript needed them in the first place. The Promise Pool4 problem isn't teaching you an innovative, quirky JS concept; it's showing you why connection pooling exists in every database engine.

Here's a rough mapping of the study plan's sections to real-world software engineering concepts:

  • Closures → State Management
  • Basic Array Transformations → Basic skill (auxiliary); Practical Example: Data Manipulation
  • Function Transformations → Middleware Patterns
  • Promises and Time -> Async Control Flow
  • JSON -> Basic skill (auxiliary); Practical Example: Data Serialization, API Communication
  • Classes (especially in the context of Event Emitters) → Message Passing Systems
  • Bonus (Premium Locked) -> Mix of harder challenges that could've been included in the sections mentioned above; Promise Pool4 is my favourite one from this section

Pattern Recognition, Not Problem Solving

Let's dissect some problems that showcase this study plan's real value.

  1. Memoize (#2623)

Consider the Memoize challenge. What I love about it, is the fact that the best solution (that I was able to come up with)
is so straightforward, it's as if the code itself is gently telling you what it does (still, I included some comments).

This doesn't make #2623 an easy problem, by any means. I needed 2 previous iterations to make it this clean:

/**
 * @param {Function} fn
 * @return {Function}
 */
function memoize(fn) {
    // Create a Map to store our results
    const cache = new Map();

    return function(...args) {
        // Create a key from the arguments
        const key = JSON.stringify(args);

        // If we've seen these arguments before, return cached result
        if (cache.has(key)) {
            return cache.get(key);
        }

        // Otherwise, calculate result and store it
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    }
}

const memoizedFn = memoize((a, b) => {
    console.log("computing...");
    return a + b;
});

console.log(memoizedFn(2, 3)); // logs "computing..." and returns 5
console.log(memoizedFn(2, 3)); // just returns 5, no calculation
console.log(memoizedFn(3, 4)); // logs "computing..." and returns 7


// Explanantion:
// It's as if our code had access to an external database

// Cache creation
// const cache = new Map();
// - this^ uses a closure to maintain the cache between function calls
// - Map is perfect for key-value storage

// Key creation
// const key = JSON.stringify(args);
// - this^ converts arguments array into a string
// - [1,2] becomes "[1,2]"
// - we are now able to use the arguments as a Map key

// Cache check
// if (cache.has(key)) {
//     return cache.get(key);
// }
// - if we've seen these arguments before, return cached result;
// no need to recalculate
Enter fullscreen mode Exit fullscreen mode
  1. Debounce (#2627)

Imagine you're in an elevator, and there's a person frantically pressing the "close door" button repeatedly.

press press press press press

Without debouncing: The elevator would try to close the door at every single press, making the door mechanism work inefficiently and possibly break.

With debouncing: The elevator waits until the person has stopped pressing for a certain time (let's say 0.5 seconds) before actually trying to close the door. This is much more efficient.

Here's another scenario:

Imagine you're implementing a search feature that fetches results as a user types:

Without debouncing:

// typing "javascript"
'j' -> API call
'ja' -> API call
'jav' -> API call
'java' -> API call
'javas' -> API call
'javasc' -> API call
'javascr' -> API call
'javascri' -> API call
'javascrip' -> API call
'javascript' -> API call
Enter fullscreen mode Exit fullscreen mode

This would make 10 API calls. Most of them useless since the user is still typing.

With debouncing (300ms delay):

// typing "javascript"
'j'
'ja'
'jav'
'java'
'javas'
'javasc'
'javascr'
'javascri'
'javascrip'
'javascript' -> API call (only one call, 300ms after user stops typing)
Enter fullscreen mode Exit fullscreen mode

Debouncing is like telling your code: "Wait until the user has stopped doing something for X milliseconds before actually running this function."

Here's the solution to LeetCode #2627:

/**
 * @param {Function} fn
 * @param {number} t milliseconds
 * @return {Function}
 */
var debounce = function (fn, t) {
  let timeoutID = null;
  return function (...args) {
    clearTimeout(timeoutID); // MDN reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout
    timeoutID = setTimeout(() => {
      fn.apply(this, args);
    }, t);
  };
};

// tests
const log = debounce(console.log, 100);

console.log("Starting tests...");

setTimeout(() => log("Test 1"), 50); // should be cancelled
setTimeout(() => log("Test 2"), 75); // should be cancelled
setTimeout(() => log("Test 3"), 200); // should be logged at t=300ms

setTimeout(() => {
  console.log("Tests completed.");
}, 400);

// Explanantion:
// - we create a closure where timeoutID persists between calls
// - timeoutID starts out as null
// - we return the debounced version of the initial function

// const log = debounce(console.log, 100);
// - this^ creates a new closure with its own timeoutID
// - log is now the inner function

// setTimeout(() => log("Test 1"), 50);  // at t=50ms
// setTimeout(() => log("Test 2"), 75);  // at t=75ms
// setTimeout(() => log("Test 3"), 200); // at t=200ms

// t=50ms: 
// - log("Test 1") called
// - clears timeoutId (null, so nothing happens)
// - sets new timeout to run at t=150ms

// t=75ms: 
// - log("Test 2") called
// - clears previous timeout (cancels "Test 1")
// - sets new timeout to run at t=175ms

// t=200ms:
// - log("Test 3") called
// - clears previous timeout (cancels "Test 2")
// - sets new timeout to run at t=300ms

// t=300ms:
// - finally executes fn("Test 3")


// why this works:
// - the closure keeps timeoutId alive between calls
// - every new call cancels the previous timeout
// - only the last scheduled timeout actually runs
Enter fullscreen mode Exit fullscreen mode

Other common real-world use cases for debouncing (apart from search bars):

  • Save drafts (wait until user stops editing)
  • Submit button (prevent double submissions)

What It Gets Wrong

I hope that, from the overall positive tone of this article, my opinion on 30 Days of JS has become clear by now.

But no educational resource is perfect, and, when it comes to limitations, honesty is valuable. This study plan has several blind spots worth examining.

First, the study plan assumes a certain level of prior knowledge.
If you're not already comfortable with JavaScript, some of the challenges can be overwhelming. This can be discouraging for beginners who might have had other expectations from the study plan.

Second of all, the challenges are presented in an isolated manner.
This makes sense in the beginning, but can be a disappointing thing to realize as you progress through the plan. Real-world problems often require combining multiple patterns and techniques. The study plan could benefit from more integrated challenges that require using several concepts together (exception: we do use closures all throughout the plan). These could fit well in the Bonus section (which is already reserved to premium users).

At last, the main weakness of this set of challenges lies in its concept explanations. Coming from competitive programming,
I'm used to clear definitions of new terms and concepts in problem statements. However, LeetCode's descriptions are often unnecessarily complex - understanding their explanation of a debounced function is harder than implementing the actual solution.

Despite its shortcomings, the study plan is a valuable resource for understanding modern JavaScript.

Beyond the 30 Days

Understanding these patterns is just the beginning.
The real challenge is recognizing when and how to apply them in production code. Here's what I've discovered after encountering these patterns in the wild.

First, these patterns rarely appear in isolation. Real codebases combine them in ways the challenges can't explore. Consider a search feature, implemented from scratch. You might find yourself using:

  • Debounce for input handling
  • Memoization for result caching
  • Promise timeouts for API calls
  • Event emitters for search state management

All these patterns interact, creating complexity that no single challenge prepares you for. But, having implemented each piece yourself, you get a general idea of how the whole implementation is supposed to function.

Counterintuitively, the most valuable skill you will gain isn't implementing these patterns - it is recognizing them in other people's code.

Final Thoughts

After completing this study plan, coding interviews aren't the only place where you'll recognize these patterns.

You'll spot them in open source code, in your colleagues' pull requests, and might start noticing them in your past projects. You might had implemented them before, without even realizing it. Most importantly, you'll understand why they're there.

What started as puzzle-solving transformed into a deeper understanding of modern JavaScript's ecosystem.

That's the gap this study plan fills: bridging theoretical knowledge with practical engineering wisdom.



  1. 2627. Debounce (Promises and Time) 

  2. 2694. Event Emitter (Classes) 

  3. 2623. Memoize (Function Transformations) 

  4. 2636. Promise Pool (Bonus) 

Top comments (0)