Tutorial Hell is a Lie: Why I Force Juniors to Build ‘Trash’
9 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.

Questions readers ask

Why do coding bootcamps and tutorials fail to teach React properly?

Binge-watching coding tutorials feels productive but doesn’t build real neural pathways — it’s like watching a marathon instead of running one. Developers finish multiple React bootcamps yet freeze when asked to build a simple fetch-and-display component because they never learned to start without a voice telling them which file to create first. Watching someone else flex their skills doesn’t transfer to you.

How do you fix the race condition in a React search bar without using lodash?

Use a useRef to track the latest query the user cares about. Inside useEffect, store the current query in latestQueryRef, then wrap the fetch in a setTimeout for 500ms debouncing. When the response returns, only call setResults if latestQueryRef.current still equals the query that fired the request. This prevents stale responses from overwriting newer results during fast typing.

Are 85 small React projects better than building one big clone app?

Yes, tiny focused projects beat massive tutorial clones because you own every line of code with nowhere to hide. Cloning Netflix usually means copy-pasting architectural decisions you don’t understand, while building something like a Hex Color Generator forces genuine ownership. The article’s author tested this by building a frantic clicking game rendering 10,000 divs to learn the React 19 Compiler rather than reading docs.

Why is the React 19 Compiler slower when I already use useMemo?

Manual memoization can actively fight the Compiler. The author saw a heavy component render in 12ms with manual useMemo but jump to 18ms when relying on the Compiler — because he was mutating a localized object inside a loop before returning it. The Compiler de-opted the optimization since it couldn’t guarantee immutability. The fix is cleaner, non-mutating code, not more memoization.

Leave a Reply

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