Component Model

Components are functions that return JSX. That’s it. No classes, no lifecycle methods, no this. A component receives props (an object of inputs) and returns a description of what should appear on screen.

Functions Returning JSX

function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}

// Arrow functions work the same way
const Badge = ({ count }: { count: number }) => (
  <span className="badge">{count}</span>
);

JSX compiles to React.createElement calls (or the automatic JSX transform in React 17+). This means JSX is just JavaScript expressions - you can assign them to variables, return them from functions, put them in arrays.

function UserCard({ user }: { user: User }) {
  const avatar = <img src={user.avatar} alt={user.name} />;
  const role = user.isAdmin ? <Badge count={99} /> : null;

  return (
    <div className="card">
      {avatar}
      <h2>{user.name}</h2>
      {role}
    </div>
  );
}

Props and Children

Props flow one way: parent to child. Never mutate them.

// Explicit props
function Button({ variant, onClick, children }: {
  variant: "primary" | "secondary";
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {children}
    </button>
  );
}

// Usage - children is whatever goes between the tags
<Button variant="primary" onClick={handleSave}>
  Save Changes
</Button>

children is just a prop. You can pass anything renderable: strings, elements, arrays, even functions (render props pattern).

// Render prop - child is a function
function DataProvider({ children }: { children: (data: Item[]) => React.ReactNode }) {
  const data = useSomeData();
  return <>{children(data)}</>;
}

<DataProvider>
  {(items) => <ItemList items={items} />}
</DataProvider>

Composition Over Inheritance

React has no component inheritance. Use composition instead.

// Bad: "inheriting" from a base component (don't do this)
// class FancyButton extends Button { ... }

// Good: compose components together
function IconButton({ icon, children, ...props }: IconButtonProps) {
  return (
    <Button {...props}>
      <Icon name={icon} />
      {children}
    </Button>
  );
}

// Good: use slots via props
function Dialog({ title, actions, children }: DialogProps) {
  return (
    <div className="dialog">
      <header>{title}</header>
      <main>{children}</main>
      <footer>{actions}</footer>
    </div>
  );
}

<Dialog
  title={<h2>Confirm Delete</h2>}
  actions={<><Button>Cancel</Button><Button variant="danger">Delete</Button></>}
>
  <p>This action cannot be undone.</p>
</Dialog>

Server vs Client Components

React 19 introduces a component split based on where they run.

AspectServer ComponentClient Component
DirectiveNone (default)"use client" at top of file
Runs onServer onlyServer (SSR) + Client (hydration)
JS sent to clientZeroYes, full component code
Can use hooksNoYes
Can use browser APIsNoYes
Can access DB/filesystemYesNo
Can import client componentsYesYes
Can import server componentsYesNo (pass as children instead)
// ServerList.tsx - no directive, runs on server only
import { db } from "@/lib/db";

async function ServerList() {
  const items = await db.items.findMany(); // direct DB access
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <InteractiveItem item={item} /> {/* client component */}
        </li>
      ))}
    </ul>
  );
}
// InteractiveItem.tsx - needs onClick, so it's a client component
"use client";

import { useState } from "react";

function InteractiveItem({ item }: { item: Item }) {
  const [liked, setLiked] = useState(false);
  return (
    <div onClick={() => setLiked(!liked)}>
      {item.name} {liked && "❤️"}
    </div>
  );
}

Gotcha: You can’t import a server component inside a client component. Instead, pass server components as children props to client components.

When Components Re-render

A component re-renders when:

  1. Its state changes (via useState setter or useReducer dispatch)
  2. Its parent re-renders (even if props haven’t changed - this is the big one)
  3. A context it consumes changes (via useContext)
function Parent() {
  const [count, setCount] = useState(0);
  // When count changes, Parent re-renders
  // Child ALSO re-renders even though it receives no props
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <Child /> {/* re-renders every time Parent does */}
    </div>
  );
}

Before React Compiler, you’d fix this with React.memo, useMemo, and useCallback. With the compiler enabled, React handles this automatically. See React Compiler for details.

Tip: If you’re on React 19 with the compiler, stop worrying about unnecessary re-renders. The compiler handles it. Focus on correct code, not micro-optimized code.

Next: Hooks for state and side effects in components