Skip to main content
Arthur Ha

React

JSX from Syntax to Runtime

June 13, 2026

I was using JSX for years before I looked at what it compiled to. It reads like HTML, but it is JavaScript all the way down — and several syntax rules exist only because a compiler sits between your editor and the runtime.

This post covers what JSX is, how it transforms at build time, the rules worth memorizing, and the edge cases that still trip me up in real codebases.

In this post:

  • What JSX is and why it exists
  • Classic createElement vs the automatic JSX runtime
  • Syntax rules, common patterns, and TypeScript typing
  • Gotchas (className, fragments, && with zero, inline handlers)

What is JSX?

The short answer

JSX is syntactic sugar for function calls that describe UI. It looks like HTML, but it is JavaScript.

With the classic transform, those calls are React.createElement():

TSX
// JSX syntax
const element = <h1>Hello, World!</h1>;

// Compiles to (classic runtime)
const element = React.createElement("h1", null, "Hello, World!");

The compiler (Babel, TypeScript, or SWC) transforms JSX before your bundle runs.

Why JSX exists

Without JSX (verbose, easy to mis-nest):

TSX
const element = React.createElement(
  "div",
  { className: "greeting", id: "main" },
  React.createElement("h1", null, "Hello"),
  React.createElement("p", null, "Welcome!"),
);

With JSX (declarative, structure matches the tree):

TSX
const element = (
  <div className="greeting" id="main">
    <h1>Hello</h1>
    <p>Welcome!</p>
  </div>
);

JSX keeps nesting visible and matches how you think about component trees.

How JSX compiles

The transformation pipeline

Original JSX:

TSX
function Greeting({ name }: { name: string }) {
  return (
    <div className="container">
      <h1>Hello, {name}!</h1>
      <p>Welcome back.</p>
    </div>
  );
}

Step 1 — Parse. The compiler turns JSX into an AST (abstract syntax tree).

Step 2 — Transform. With the classic runtime, the AST becomes createElement calls:

TSX
function Greeting({ name }: { name: string }) {
  return React.createElement(
    "div",
    { className: "container" },
    React.createElement("h1", null, "Hello, ", name, "!"),
    React.createElement("p", null, "Welcome back."),
  );
}

Step 3 — Execute. At runtime, createElement (or the automatic runtime helpers) returns a plain object React uses during reconciliation — often called a React element:

TSX
// Shape returned by createElement (simplified)
{
  $$typeof: Symbol.for('react.element'),
  type: 'div',
  props: {
    className: 'container',
    children: [
      { type: 'h1', props: { children: ['Hello, ', 'Alice', '!'] } },
      { type: 'p', props: { children: 'Welcome back.' } },
    ],
  },
}

Automatic JSX runtime (React 17+)

Most apps today use the automatic transform. You do not need import React from 'react' just to write JSX, and the compiler emits calls to jsx / jsxs from react/jsx-runtime instead of React.createElement:

TSX
// Compiles to (automatic runtime, simplified)
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";

// Same JSX
function Greeting({ name }: { name: string }) {
  return (
    <div className="container">
      <h1>Hello, {name}!</h1>
      <p>Welcome back.</p>
    </div>
  );
}

function Greeting({ name }) {
  return _jsxs("div", {
    className: "container",
    children: [
      _jsx("h1", { children: ["Hello, ", name, "!"] }),
      _jsx("p", { children: "Welcome back." }),
    ],
  });
}

The mental model is the same: JSX becomes function calls that return React elements. Only the import path and helper names changed.

createElement signature (classic runtime)

TSX
// Under the hood, JSX calls this:
React.createElement(
  type,      // 'div', Component, Fragment, etc.
  props,     // Object with attributes/event handlers
  ...children // Variable number of children
)

// Returns:
{
  type,
  props: { ...props, children },
  key,
  ref,
  // ... other internal fields
}

JSX → createElement mapping

JSXcreateElement
<div/>React.createElement("div")
<Component />React.createElement(Component)
<div className="foo">Hello</div>React.createElement("div", {className: "foo" }, "Hello")
<div>{children}</div>React.createElement("div", null, children)
<div>A {expr} B</div>React.createElement("div", null, "A ", expr, " B")

Syntax rules

Rule 1: Children live in props.children

TSX
// ❌ WRONG: You can't access children like props.name
function Component(props) {
  return <div>{props.name}</div>; // ✓ name is a prop
}

// ✓ CORRECT: Children are in props.children
function Component(props) {
  return <div>{props.children}</div>;
}

// Better: Destructure
function Component({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

// Usage:
<Component>This is children</Component>;

Under the hood:

TSX
// This JSX:
<Component>Hello</Component>;

// Compiles to:
React.createElement(Component, null, "Hello");

// Which means props = { children: 'Hello' }

Rule 2: Props are attributes, handlers, or React-specific props

TSX
// HTML Attributes
<div className="red" id="main" data-testid="header" />

// Event Handlers (camelCase!)
<button onClick={handleClick} onMouseEnter={handleHover} />

// Special Props (React-specific)
<Component key="unique-id" ref={myRef} />

// Custom Props (passed to component)
<Component isLoading={true} userName="Alice" theme="dark" />

Rule 3: Capitalization matters

TSX
// ✓ Lowercase = HTML element
<div>This is a div</div>
<span>This is a span</span>

// ✓ Capitalized = React Component
`<Component />`
`<MyButton />`
`<UserProfile />`

// ❌ This is a BUG (React treats it as HTML element, not component!)
const myComponent = () => <div>Hello</div>;
<myComponent />  // Wrong! React looks for <mycomponent> HTML element

// ✓ CORRECT: Assign to capitalized variable
const MyComponent = () => <div>Hello</div>;
<MyComponent />  // Correct!

// Why? Babel uses this rule:
// - Lowercase with dots (foo.bar) → dynamic import
// - Lowercase without dots (foo) → HTML element string
// - Capitalized (Foo) → component reference

Rule 4: Curly braces take expressions, not statements

TSX
// ✓ VALID: Expressions evaluate to values
<div>{name}</div>
<div>{count + 1}</div>
<div>{isReady ? 'Ready' : 'Loading'}</div>
<div>{items.map(item => <li key={item.id}>{item.name}</li>)}</div>

// ❌ INVALID: if/for statements (not expressions)
<div>
  {if (isReady) { return 'Ready'; }}  // ❌ Syntax error!
</div>

// ✓ VALID: Use conditional expressions instead
<div>
  {isReady ? 'Ready' : 'Loading'}
</div>

// ❌ INVALID: Objects (must serialize or destructure)
<div>{name: 'Alice', age: 30}</div>  // ❌ Error!

// ✓ VALID: Serialize or pick properties
<div>{JSON.stringify(user)}</div>
<div>{user.name}, {user.age}</div>

// ❌ INVALID: Functions aren't rendered
<div>{() => 'text'}</div>  // ❌ Shows "[Function]"

// ✓ VALID: Call the function
<div>{getGreeting()}</div>

// ❌ INVALID: Promises aren't rendered (yet—Suspense changes this)
<div>{fetchData()}</div>  // ❌ Shows "[object Promise]"

// ✓ VALID: Use useEffect to handle promise
useEffect(() => {
  fetchData().then(data => setData(data));
}, []);

Rule 5: Props are read-only

TSX
// ✓ Props are read-only
function Component({ count }: { count: number }) {
  return <div>{count}</div>;
}

// ❌ WRONG: Don't modify props
count = count + 1; // ❌ Error! const

// ✓ CORRECT: Use state for mutation
const [count, setCount] = useState(0);
<div onClick={() => setCount(count + 1)}>{count}</div>;

Rule 6: null, undefined, true, and false render nothing

TSX
// These render nothing:
<div>{null}</div>           // Empty
<div>{undefined}</div>      // Empty
<div>{true}</div>           // Empty
<div>{false}</div>          // Empty

// ✓ CORRECT: Render with &&
<div>{isReady && 'Ready'}</div>

// ❌ WRONG: This renders "false"
<div>{isReady || 'Loading'}</div>  // If false, shows "Loading" ✓
<div>{!isReady && 'Loading'}</div> // Better: shows nothing if true

// ⚠️ GOTCHA: Be careful with && and falsy numbers
<div>{count && <Items count={count} />}</div>
// If count = 0, renders "0" (not empty!)

// ✓ FIX: Use explicit check
<div>{count > 0 && <Items count={count} />}</div>

Common patterns

Conditional rendering

TSX
// ✓ Ternary (best for binary choice)
<div>
  {isLoading ? <LoadingSpinner /> : <Content />}
</div>

// ✓ && (for show/hide)
<div>
  {isReady && <ReadyContent />}
</div>

// ✓ if/else outside JSX (for complex logic)
let content;
if (isLoading) {
  content = <LoadingSpinner />;
} else if (error) {
  content = <ErrorMessage error={error} />;
} else {
  content = <Content />;
}

return <div>{content}</div>;

// ✓ Switch/case (for multiple states)
const getContent = () => {
  switch (status) {
    case 'loading':
      return <LoadingSpinner />;
    case 'error':
      return <ErrorMessage />;
    case 'success':
      return <Content />;
  }
};

return <div>{getContent()}</div>;

Rendering lists

TSX
// ✓ CORRECT: Use .map() with unique key
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// ❌ WRONG: Using index as key
{
  users.map((user, index) => <li key={index}>{user.name}</li>);
}

// ✓ Filter within map
{
  users
    .filter((user) => user.isActive)
    .map((user) => <li key={user.id}>{user.name}</li>);
}

// ✓ Complex filtering
{
  users
    .filter((user) => user.age > 18)
    .sort((a, b) => a.name.localeCompare(b.name))
    .map((user) => <li key={user.id}>{user.name}</li>);
}

// ⚠️ GOTCHA: Creating arrays in JSX
{
  user.roles.map((role) => <Tag key={role}>{role}</Tag>);
}
// If role is string, this works. If it's object, include an ID!

Dynamic props

TSX
// ✓ Spread operator
<Component {...props} />

// ✓ Conditional props
<button disabled={isLoading} onClick={handleClick}>
  {isLoading ? 'Loading...' : 'Submit'}
</button>

// ✓ Dynamic className
<div className={`btn ${isActive ? 'active' : ''}`}>Button</div>

// ✓ Dynamic style object
<div style={{
  color: isDark ? 'white' : 'black',
  padding: spacing * 2
}}>
  Content
</div>

// ✓ Dynamic event handlers
const handler = isEditing ? handleSave : handleEdit;
<button onClick={handler}>
  {isEditing ? 'Save' : 'Edit'}
</button>

Composition

TSX
// ✓ Children pattern
function Container({ children }: { children: React.ReactNode }) {
  return <div className="container">{children}</div>;
}

<Container>
  <Header />
  <Content />
  <Footer />
</Container>;

// ✓ Slots pattern (multiple children)
function Layout({
  sidebar,
  main,
  footer,
}: {
  sidebar: React.ReactNode;
  main: React.ReactNode;
  footer: React.ReactNode;
}) {
  return (
    <div className="layout">
      <div className="sidebar">{sidebar}</div>
      <div className="main">{main}</div>
      <div className="footer">{footer}</div>
    </div>
  );
}

<Layout sidebar={<Sidebar />} main={<MainContent />} footer={<Footer />} />;

// ✓ Render props pattern
function DataProvider({
  children,
}: {
  children: (data: Data) => React.ReactNode;
}) {
  const [data, setData] = useState<Data | null>(null);
  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data && children(data)}</div>;
}

<DataProvider>{(data) => <UserProfile user={data} />}</DataProvider>;

TypeScript and JSX

Basic type safety

TSX
// ✓ Typed props
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

// ✓ Usage is type-safe
<Button label="Click me" onClick={() => console.log('clicked')} />

// ❌ TypeScript error:
<Button label="Click me" onClik={() => {}} />  // Property 'onClik' does not exist

// ✓ React.ReactNode for children
interface ComponentProps {
  children?: React.ReactNode;
  title: string;
}

function Card({ children, title }: ComponentProps) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Typing event handlers

TSX
// ✓ Specific event types
function TextInput() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("clicked");
  };

  return (
    <>
      <input onChange={handleChange} />
      <form onSubmit={handleSubmit}>
        <button onClick={handleClick}>Submit</button>
      </form>
    </>
  );
}

// Common event types:
// React.ChangeEvent<T>       - onChange
// React.FormEvent<T>         - onSubmit
// React.MouseEvent<T>        - onClick, onHover
// React.KeyboardEvent<T>     - onKeyDown, onKeyUp
// React.FocusEvent<T>        - onFocus, onBlur

Generic components

TSX
// ✓ Generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// ✓ Usage with type inference
<List
  items={[
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ]}
  renderItem={(user) => `${user.name}`}
  keyExtractor={(user) => user.id}
/>;

// Without explicit type, TypeScript infers:
// T = { id: number; name: string }

Prop spreading

TSX
// ✓ Forward all props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary";
}

function Button({ variant = "primary", ...props }: ButtonProps) {
  return <button className={`btn-${variant}`} {...props} />;
}

// ✓ Usage preserves all standard button props
<Button
  variant="primary"
  onClick={() => {}}
  disabled={true}
  type="submit"
  className="custom"
/>;

// ✓ Extract known props, pass rest
interface CardProps {
  title: string;
  children?: React.ReactNode;
  // Rest are HTML div attributes
}

function Card({
  title,
  children,
  ...divProps
}: CardProps & React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Edge cases and gotchas

Fragment shorthand vs full syntax

TSX
// ✓ Shorthand (recommended)
<>
  <h1>Title</h1>
  <p>Content</p>
</>

// ✓ Full syntax (needed with key or props)
<React.Fragment key="section1">
  <h1>Title</h1>
  <p>Content</p>
</React.Fragment>

// ✓ With keys (can't use shorthand)
{items.map(item => (
  <React.Fragment key={item.id}>
    <h3>{item.title}</h3>
    <p>{item.description}</p>
  </React.Fragment>
))}

// Compiles to:
React.createElement(React.Fragment, null, ...)

Strings vs numbers (and the && trap)

TSX
// ✓ String is rendered as text
<div>Hello</div>

// ✓ Number is rendered as text
<div>{42}</div>

// ⚠️ But be careful with 0
{count === 0 && <div>Count is zero</div>}  // Shows nothing ✓

// ❌ This shows "0"
{count && <div>Count is {count}</div>}     // If count = 0, shows "0" ✗

// ✓ Fix with > 0
{count > 0 && <div>Count is {count}</div>}

class vs className

TSX
// ❌ WRONG: class is reserved JavaScript keyword
<div class="container">  // ❌ This won't work in JSX

// ✓ CORRECT: Use className
<div className="container">  // ✓

// Why? Because JSX is JavaScript, and class is a keyword.
// className gets compiled to the class prop.

The style prop

TSX
// ❌ WRONG: String (in HTML you'd do style="color: red")
<div style="color: red">  // ❌ Won't work!

// ✓ CORRECT: Object with camelCase properties
<div style={{ color: 'red', fontSize: '16px' }}>
  Content
</div>

// ✓ Or define outside
const styles = {
  container: {
    color: 'red',
    fontSize: '16px',
  }
};
<div style={styles.container}>Content</div>

// ⚠️ Units: Numbers are assumed to be px for most properties
<div style={{ padding: 16 }}>  // 16px
<div style={{ opacity: 0.5 }}>  // 0.5 (unitless)
<div style={{ zIndex: 10 }}>    // 10 (unitless)

SVG attributes

TSX
// ❌ WRONG: SVG uses different attribute names
<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" stroke="black" strokeWidth="3" />
</svg>

// SVG attributes are camelCased in JSX:
// stroke-width → strokeWidth
// stroke-linecap → strokeLinecap
// fill-rule → fillRule
// xml:lang → xmlLang

htmlFor on labels

TSX
// ❌ WRONG: for is a keyword
<label for="username">Username</label>

// ✓ CORRECT: Use htmlFor
<label htmlFor="username">Username</label>
<input id="username" />

Comments in JSX

TSX
// ✓ CORRECT: {} outside comments don't work in JSX
{/* This is a comment */}

// ❌ WRONG: // comments in JSX break things
<div>
  // This breaks the JSX! ❌
</div>

// ✓ CORRECT: Use {} for multiline comments
<div>
  {/*
    Multiline comment
    in JSX works fine
  */}
  Content
</div>

Empty components

TSX
// ❌ WRONG: Returning nothing
function Component() {
  return; // ❌ Returns undefined, not empty JSX
}

// ✓ CORRECT: Return null or empty fragment
function Component() {
  return null; // ✓ Renders nothing
}

Inline functions in event handlers

TSX
// ✓ Arrow function (creates new function each render, but fine for events)
<button onClick={() => handleClick(id)}>Click</button>

// ✓ Or use useCallback to prevent re-renders of memoized children
const handleClick = useCallback(() => {
  console.log(id);
}, [id]);
<button onClick={handleClick}>Click</button>

// ❌ DON'T: Call function immediately
<button onClick={handleClick()}>Click</button>  // ❌ Calls handleClick on render!

// ❌ DON'T: Pass arguments directly without callback
<button onClick={handleDelete(item.id)}>Delete</button>  // ❌ Calls on render!

// ✓ CORRECT: Wrap in callback
<button onClick={() => handleDelete(item.id)}>Delete</button>

Intrinsics and components

Intrinsic elements (built-in HTML)

TSX
// Intrinsic elements — lowercase tags React maps to DOM nodes
<div />
<span />
<button />
<input />
<p />
// React knows their props; TypeScript uses JSX.IntrinsicElements

Custom components

TSX
// Components you define — capitalized identifiers
<MyButton />
<Card />
<UserProfile />

// React calls your function with props; TypeScript uses your props interface

Polymorphic components

TSX
// "as" pattern: Component that can be any element type
interface PolymorphicProps {
  as?: React.ElementType;
  children?: React.ReactNode;
  className?: string;
}

function Box({ as: Component = 'div', className, children }: PolymorphicProps) {
  return <Component className={className}>{children}</Component>;
}

// Usage:
<Box>Default (renders as div)</Box>
<Box as="section">Renders as section</Box>
<Box as="article">Renders as article</Box>

// Advanced: Type-safe polymorphic component
type PolymorphicPropsWithRef<E extends React.ElementType> =
  React.ComponentProps<E> & {
    as?: E;
  };

function Button<E extends React.ElementType = 'button'>(
  { as, ...props }: PolymorphicPropsWithRef<E>
) {
  const Component = as || 'button';
  return <Component {...props} />;
}

// Now typing is preserved:
<Button onClick={() => {}}>Button</Button>  // button props
<Button as="a" href="/home">Link Button</Button>  // a props

Performance

Re-renders and referential identity

Inline arrow functions in event handlers are fine for most components. Reach for useCallback when profiling shows a memoized child re-rendering unnecessarily, not by default.

TSX
// Usually fine — new function each render, but cheap for a native <button>
<button onClick={() => handleClick(id)}>Click</button>

// Worth memoizing when the child is wrapped in React.memo and re-renders are costly
const handleClickWithId = useCallback(() => {
  handleClick(id);
}, [id]);

<MemoizedButton onClick={handleClickWithId}>Click</MemoizedButton>

// ❌ Inline objects/arrays passed to memoized children — new reference every render
<MemoizedCard style={{ color: 'red' }} />

// ✓ Hoist or memoize when it matters
const style = useMemo(() => ({ color: 'red' }), []);
<MemoizedCard style={style} />

// ✓ Stable array reference — hoist literals outside render when reused
const numbers = [1, 2, 3];
{numbers.map(i => <Item key={i} number={i} />)}

Conditional rendering and mount cost

TSX
// Both are idiomatic; pick based on readability
{
  isReady ? <Content /> : <Spinner />;
}
{
  isReady && <Content />;
}

// With &&, React only mounts <Content /> when isReady is true.
// Prefer && when the fallback UI is "render nothing" rather than an alternate branch.
{
  isReady && <ExpensiveComponent />;
}

Best practices

Keep JSX readable

TSX
// ❌ TOO NESTED
return (
  <div>
    {data ? (
      <section>
        {items.length > 0 ? (
          <ul>
            {items.map((item) => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        ) : (
          <p>No items</p>
        )}
      </section>
    ) : (
      <p>Loading...</p>
    )}
  </div>
);

// ✅ EXTRACTED TO VARIABLES/FUNCTIONS
const renderContent = () => {
  if (!data) return <p>Loading...</p>;
  if (items.length === 0) return <p>No items</p>;
  return <ItemList items={items} />;
};

return <section>{renderContent()}</section>;

Name components clearly

TSX
// ✓ Capitalize component names
const UserCard = () => <div>User</div>;

// ✓ Descriptive names
const LogoutButton = () => <button>Logout</button>;
const UserAvatarWithName = () => <div>User</div>;

// ✓ Use verb-noun pattern for data fetching
const UserProfileWithData = () => {
  const { data } = useQuery(...);
  return <UserProfile user={data} />;
};

Separate data and presentation

TSX
// ✓ Presentational component (pure)
interface UserProfileProps {
  user: User;
  onEdit: () => void;
}

const UserProfile = ({ user, onEdit }: UserProfileProps) => (
  <div>
    <h1>{user.name}</h1>
    <button onClick={onEdit}>Edit</button>
  </div>
);

// ✓ Container component (smart, with data)
const UserProfileContainer = ({ userId }: { userId: string }) => {
  const { data: user } = useQuery([userId], () => fetchUser(userId));

  const handleEdit = () => {
    // Edit logic
  };

  return <UserProfile user={user} onEdit={handleEdit} />;
};

Type your props

TSX
// ✓ Always use interfaces or types
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary" | "secondary";
}

function Button({
  label,
  onClick,
  disabled = false,
  variant = "primary",
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Memoize when profiling says so

TSX
// ✓ Memoize expensive components
const UserCard = React.memo(({ user }: { user: User }) => (
  <div>
    <h2>{user.name}</h2>
    <p>{user.bio}</p>
  </div>
));

// ✓ Memoize with custom comparison
const Button = React.memo(
  ({ onClick, children }: ButtonProps) => (
    <button onClick={onClick}>{children}</button>
  ),
  (prevProps, nextProps) => {
    // Only re-render if onClick changes
    return prevProps.onClick === nextProps.onClick;
  },
);
Knowledge check

30 multiple-choice questions · test what you learned from this post

Takeaways

  • JSX is syntax sugar — it compiles to runtime helpers (jsx / jsxs today, or createElement with the classic transform).
  • Capitalization decides intrinsic vs component: <div> vs <Div>.
  • Children become props.children; expressions go in {}, not statements.
  • Type props explicitly; extend React.ComponentProps / HTMLAttributes when wrapping native elements.
  • Extract nested JSX early; keep files readable before reaching for memoization.
  • Watch the usual traps: className, object style, fragment key, && with 0, and calling handlers during render (onClick={fn()}).

Next in this series: The Fiber Tree — what React builds after JSX becomes elements.