Skip to main content
CodeOath
← All posts

React120 min total · 17 parts

React Fundamentals: Components, Hooks, and the Virtual DOM

Part 2 of 17 · ~5 min

JSX and the Rendering Model

Here is one row of the table, written the way you would actually write it:

<tr className="row" onClick={() => selectCandidate(candidate.id)}>
  <td>{candidate.name}</td>
  <td>{candidate.score}</td>
</tr>

That looks like HTML sitting in the middle of a JavaScript file, which is a thing JavaScript has no opinions about and no syntax for. It is not HTML, and it is not part of the language. JSX is syntax that a build step rewrites into ordinary function calls before the browser sees a single character of it.

Modern tooling compiles that <td> to roughly this:

import { jsx as _jsx } from "react/jsx-runtime";

const cell = _jsx("td", { children: candidate.name });

Older tooling — and every tutorial written before 2020 — compiles it to React.createElement("td", null, candidate.name) instead. The two differ in mechanical detail: the newer "automatic" runtime imports its helper for you, which is why you no longer need import React from "react" at the top of every file. What they produce is identical, and it is the part that matters.

What they produce is a plain JavaScript object. Not a DOM node, not something attached to the page:

{
  type: "td",
  props: { children: "Priya Raghunathan" },
  key: null,
  // ...plus internal fields React uses and you should never read
}

That object is called a React element, and it is a description of what should be on screen, the way a recipe is a description of a cake. Creating one costs a property lookup and an object allocation. Throwing it away costs nothing. This is load-bearing: when the reviewer types one character into the filter box, React rebuilds the element for every visible row, and that is fine, because it is building descriptions rather than touching the page. Chapter 4 is about what it does with them.

A handful of JSX rules trip people specifically because the syntax looks like HTML, so the differences arrive unannounced. All four of them show up in this table.

A component returns exactly one root node. Our row needs to return several <td> elements as siblings, and the usual advice — wrap them in a <div> — is illegal here, because a <div> is not a valid child of <tr>. That is what Fragments are for: a wrapper that groups elements for React without producing any DOM at all.

function RowCells({ candidate }) {
  return (
    <>
      <td>{candidate.name}</td>
      <td>{candidate.role}</td>
      <td>{candidate.score}</td>
    </>
  );
}

<>...</> is shorthand for <React.Fragment>...</React.Fragment>. You need the long form in exactly one situation — when the fragment needs a key, which happens when you map over data and each iteration produces several siblings. The shorthand accepts no attributes at all.

class is className, and for is htmlFor. Both are reserved words in JavaScript, and JSX attributes become object keys in that props object, so the names had to change. Our filter bar hits the second one:

<label htmlFor="min-score">Minimum score</label>
<input id="min-score" className="filter-input" type="number" />

Everything inside {} must be an expression, not a statement. {} means "evaluate this and use the result", and if does not evaluate to anything. So this is a syntax error:

<td>{if (candidate.stage === "hired") { ... }}</td>   // no

Your three options are a ternary, &&, or — usually the most readable once there are more than two branches — pulling the decision out above the return into a plain variable or an early return.

React renders anything that is a string or a number, and skips null, undefined, true and false. That rule is easy to nod along with and then get bitten by, because of one specific interaction with &&. We want a coloured score badge, but only for candidates who have a score:

<td>{candidate.score && <ScorePill score={candidate.score} />}</td>

That works for everyone except the candidate who scored zero, whose cell now contains a bare 0. Not a badge, not blank — the literal character 0.

Follow it through. && does not return a boolean; it returns one of its operands. candidate.score is 0, which is falsy, so the whole expression evaluates to 0 and never reaches the pill. React is then handed the number 0 as a child, and numbers are things React renders. Had the score been null, the expression would have evaluated to null and rendered nothing at all — which is precisely why this bug hides so well. It only appears for the one candidate who genuinely earned a zero.

Write the condition as an actual boolean and it goes away:

<td>{candidate.score !== null && <ScorePill score={candidate.score} />}</td>

The habit worth building: any time the left side of && could be 0 or "", convert it to a real comparison. list.length && <List /> on an empty list has the identical bug and shows up far more often than the score one.

Conditional and list rendering

JSX has no if and no for. It does not need them, because it is an expression language embedded in JavaScript, and JavaScript already has ternaries and .map().

function StagePill({ stage }) {
  return <span className={`pill pill--${stage}`}>{STAGE_LABELS[stage]}</span>;
}

function EmptyState({ hasFilters }) {
  return (
    <p className="empty">
      {hasFilters
        ? "No candidates match these filters."
        : "No submissions yet."}
    </p>
  );
}

function CandidateTable({ candidates }) {
  if (candidates.length === 0) return <EmptyState hasFilters />;

  return (
    <tbody>
      {candidates.map((candidate) => (
        <CandidateRow key={candidate.id} candidate={candidate} />
      ))}
    </tbody>
  );
}

Three things happening there worth naming. The ternary handles a two-way choice inline. The early return handles the branch that replaces the whole output — far easier to read than nesting the entire table inside a ternary. And .map() produces an array of elements, which React renders happily, as long as each element carries a key.

That key={candidate.id} is not a formality, and it is not there to silence a console warning. It is the single most consequential attribute in this chapter, it is about to leak one reviewer's notes onto the wrong person's record, and chapter 4 is where we take it apart.