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.
| Aspect | Server Component | Client Component |
|---|---|---|
| Directive | None (default) | "use client" at top of file |
| Runs on | Server only | Server (SSR) + Client (hydration) |
| JS sent to client | Zero | Yes, full component code |
| Can use hooks | No | Yes |
| Can use browser APIs | No | Yes |
| Can access DB/filesystem | Yes | No |
| Can import client components | Yes | Yes |
| Can import server components | Yes | No (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
childrenprops to client components.
When Components Re-render
A component re-renders when:
- Its state changes (via
useStatesetter oruseReducerdispatch) - Its parent re-renders (even if props haven’t changed - this is the big one)
- 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