Contents Lesson 6 of 16

10 min read · foundations

Read what your assistant wrote

The previous lesson said READ is the step that makes you a programmer. This is the practice: a generated watchlist fetch with real problems in it, read the way you should read every diff.

Here is what an assistant produced for "fetch quotes for my watchlist and show price and change". It runs. It looks fine.

"use client";
import { useEffect, useState } from "react";

export function Watchlist({ tickers }: { tickers: string[] }) {
  const [rows, setRows] = useState<any[]>([]);

  useEffect(() => {
    tickers.forEach(async (t) => {
      const res = await fetch(
        `https://eodhd.com/api/real-time/${t}?fmt=json&api_token=${process.env.NEXT_PUBLIC_EODHD_API_KEY}`
      );
      const data = await res.json();
      setRows((prev) => [...prev, data]);
    });
  }, [tickers]);

  return (
    <table>
      {rows.map((r) => (
        <tr key={r.code}>
          <td>{r.code}</td>
          <td>{r.close}</td>
          <td>{r.change_p}%</td>
        </tr>
      ))}
    </table>
  );
}

Before you can judge it — what you are looking at

If you came from course 0 this is the first React you have seen, and every criticism below depends on five ideas. They are the last unfamiliar vocabulary in this course.

A component is a function that returns markup. Watchlist is an ordinary function whose return value is unusual: that HTML-looking thing is JSX, a description of what should be on screen. The function runs, returns the description, and React puts it there.

Props are its arguments. { tickers } is one parameter, destructured: <Watchlist tickers={["AAPL.US"]} /> is calling the function with that value.

State is a value the component remembers between calls, and changing it calls the function again. const [rows, setRows] = useState([]) gives you the current value and the way to change it. setRows(...) does not just assign; it tells React the answer has changed, so the function runs again and returns new markup. That re-run is a render, and it is why the append bug below matters: a function that runs many times must produce the same result each time from the same inputs.

useEffect means "after rendering, do this." Fetching is not part of describing the screen, so it does not belong in the function body: useEffect(() => {...}, [tickers]) runs after the render, and again whenever tickers changes.

Server or browser: the code looks identical and the consequences do not. The same component can run on your server, which sends finished HTML to the visitor, or in the visitor's browser. "use client" on the first line picks the browser, and everything your code can read there, the visitor can read too.

That is the whole model.

Read the code again, then work through the four questions before reading on.

Question 2 first: does it touch data fetching?

It does, and that is where the fatal bug is. Two lines conspire:

"use client";
...api_token=${process.env.NEXT_PUBLIC_EODHD_API_KEY}

This component runs in the browser, and the assistant reached for a NEXT_PUBLIC_ variable because that is the only kind a browser can read. Each half is reasonable, which is why the pattern is common; together they publish your key to every visitor, in plain text, in a URL that also lands in logs.

There is no partial fix. The fetch has to move behind the proxy:

const res = await fetch(`/api/proxy/real-time/${first}?s=${rest.join(",")}`);

No key in sight, because the server adds it. This single question catches the worst class of bug this course can produce.

Question 1: did it change anything I did not ask for?

It made the component client-side, which nobody asked for. That followed from fetching in useEffect, and it dragged in browser-only state, a loading flicker and the key problem above, all three of which server-side rendering would have avoided.

This is the quiet architectural decision the previous lesson told you to forbid in the prompt.

Question 3: what happens when it fails?

Nothing good, in four separate ways.

One request per ticker. Twelve instruments, twelve round trips, every render. The multi-symbol form does the same job in one request, and since /real-time is billed per symbol it still costs twelve calls: what you save is twelve round trips, twelve rate-limit slots and twelve chances to fail differently, not quota.

res.ok is never checked. A 402 for a plan that does not cover the ticker, a 429 for too many calls: res.json() runs anyway and pushes an error object into the table, which renders as blank cells and looks broken with no clue why.

The single-ticker shape. With one ticker, /real-time returns a bare object rather than an array, and data is pushed into rows either way, so a one-item list renders one strange row.

The append race. setRows(prev => [...prev, data]) inside a forEach of async callbacks means rows arrive in whatever order responses land, and a re-render duplicates the list rather than replacing it.

Question 4: can I explain it?

You now can, including everything wrong with it, which is the standard. The code was not stupid: every choice was locally plausible, and it was wrong as a whole, which only reading it as a whole finds.

What to do with a diff like this

Not "fix it line by line", because then you are doing the assistant's job. Reject it with specifics, since a specific rejection is a better prompt:

This puts the key in the browser: NEXT_PUBLIC_ plus "use client". Redo it server-side. Fetch through /api/proxy/real-time/... using the multi-symbol form so the whole list is one request. Handle a non-ok response and the single-ticker bare-object case explicitly.

Four sentences, and the next version is usually right. That is the skill of the whole track: not writing the code, but knowing precisely what is wrong with the code you were handed.

Try it now

Take the code above into your own assistant and ask it to review it as someone else's work, before telling it what you found. Compare its list with the five problems here, and where it missed something, ask why: the gaps in its review are a map of what you cannot delegate.