Tutorial Hell is a Lie: Why I Force Juniors to Build ‘Trash’
7 mins read

Tutorial Hell is a Lie: Why I Force Juniors to Build ‘Trash’

Actually, I should clarify – the resume looked perfect, and his GitHub was green squares all the way down. He’d even finished three separate “Master React 19” bootcamps and had the certificates to prove it.

Then I asked him to build a simple fetch-and-display component. No styling, just get a list of users from an API and render them.

He froze. But — he actually opened a new tab and started typing youtube.com before he caught himself. He didn’t know how to start without a voice telling him which file to create first.

This is what happens when you binge-watch coding content like it’s Netflix. You feel productive, you feel like you’re learning. But you aren’t building neural pathways; you’re just watching someone else flex theirs. It’s the difference between watching a marathon and running one.

I’ve been mentoring devs for a decade, and I’ve stopped recommending courses. Completely. If you want to actually learn React in 2026 — especially with how much the ecosystem has shifted since the React 19 release — you need to build garbage. Lots of it.

The “85 Mini Projects” Theory

There’s this idea floating around that building 85 tiny, focused projects is infinitely better than building one massive “Clone Netflix” app following a tutorial. And it’s correct. (Fight me.)

When you build a massive clone, you’re usually copy-pasting architecture decisions you don’t understand. But when you build a tiny app — say, a “Hex Color Generator” — you have to own every single line of code. There’s nowhere to hide.

I tried this myself recently. I wanted to get comfortable with the new React Compiler optimizations (which, by the way, are amazing but occasionally weird). And instead of reading the documentation for three hours, I built a frantic little clicking game.

The goal? Render 10,000 div elements and see if I could break the frame rate on my M3 Air. Spoiler: I could, but it took way more effort than it used to.

Stop Building To-Do Lists (Please)

The problem with most “beginner projects” is that they are boring. A To-Do list teaches you CRUD, sure. But it doesn’t teach you about race conditions, browser APIs, or state synchronization issues that actually crash production apps.

Here are three “mini” projects I assign to juniors now. They sound simple. They aren’t.

  • The “Bad” Search Bar: Build an input that searches an API. But here’s the catch: you have to handle the race condition where the first request comes back after the second request. If I type “React” and then “Vue”, but the “React” results load last, the UI shouldn’t show “React” results while the input says “Vue”.
  • The Memory Game: A grid of cards. Flip two. If they match, they stay. If not, they flip back. Sounds easy? Managing the state transitions and timeouts without introducing bugs where a user can flip a third card while the others are animating is a nightmare. It forces you to understand useEffect cleanup and state locking.
  • The Infinite Scroller (No Libraries): Load data as the user scrolls. You have to touch the Intersection Observer API. You have to handle loading states. You have to handle the case where the API returns no more data.

A Real-World Example: The Debounce Trap

Let’s look at that search bar idea. A tutorial would tell you to just install lodash.debounce and call it a day. But if you build this yourself, you learn why we need it.

I wrote this snippet for a project last week running on Node 22.13.0. And I wanted to handle search input without external libraries, just using modern React hooks.

React javascript code on monitor - Programming language Images - Free Download on Freepik
React javascript code on monitor – Programming language Images – Free Download on Freepik
import { useState, useEffect, useRef } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);
  
  // We use a ref to track the latest query we actually care about
  // This prevents the "stale response" race condition
  const latestQueryRef = useRef(query);

  useEffect(() => {
    latestQueryRef.current = query;

    if (!query) {
      setResults([]);
      return;
    }

    setIsSearching(true);
    
    // The native debounce implementation
    const timerId = setTimeout(async () => {
      try {
        // Simulating an API call
        const response = await fetch(https://api.example.com/search?q=${query});
        const data = await response.json();
        
        // THE CRITICAL CHECK:
        // Only update state if this is still the query the user wants
        if (latestQueryRef.current === query) {
          setResults(data);
          setIsSearching(false);
        }
      } catch (err) {
        console.error("Fetch failed", err);
        setIsSearching(false);
      }
    }, 500); // 500ms delay

    return () => clearTimeout(timerId);
  }, [query]);

  return (
    <div className="p-4">
      <input 
        type="text" 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search users..."
        className="border p-2 rounded w-full"
      />
      {isSearching && <p className="text-gray-500">Searching...</p>}
      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    <div>
  );
}

See that latestQueryRef check? That single line is the difference between a junior dev’s code and a senior’s. A tutorial might gloss over that. But when you build it and see your UI flickering with wrong data, you are forced to invent that check (or Google it frantically).

My “React 19” Reality Check

Speaking of building things yourself, I ran into a nasty edge case recently while testing the React Compiler (beta channel at the time, now standard in 19.2). And I was so used to manually memoizing everything with useMemo that I was actually fighting the compiler.

I had a heavy calculation component. With manual memoization, it rendered in about 12ms. But when I removed the memoization to let the Compiler handle it, it jumped to 18ms. Why?

Turns out, I was mutating a localized object inside a loop before returning it. The compiler played it safe and de-opted the optimization because it couldn’t guarantee immutability. And I spent two hours blaming the tool before I realized my code was just sloppy. If I had just followed a “What’s New in React 19” guide, I would have nodded along and assumed the Compiler is magic. But it isn’t. It’s software.

Just Build The Thing

You don’t need another Udemy course. You need to open VS Code, create a new folder, and try to build something that scares you slightly.

Start small. Build a stopwatch. Then build a stopwatch that saves your laps to localStorage. Then build a stopwatch that syncs laps across tabs using the Broadcast Channel API. And by the time you finish that third iteration, you’ll understand state management better than any certificate can prove.

And if you get stuck? Good. That frustration is the actual learning happening. Embrace the suck.

Leave a Reply

Your email address will not be published. Required fields are marked *