React 19 Component Patterns: A Modern Guide
A practical guide to the core component patterns in modern React, focusing on server-first architecture, predictable rendering, and maintainable UI design.
React 19 Component Patterns: A Modern Guide
Modern React is built around a few core ideas: predictable rendering, stable component identity, server-first data loading, and a very small set of escape hatches for imperative logic.
If you understand these patterns, you can reason about complex apps without relying on tricks or outdated approaches. This article walks through the key component patterns you should use in React 19 projects, with practical examples and clear boundaries for when to apply each one.
1. Server Components for Data and Structure
In modern React, data loading belongs on the server by default. Server components:
- run only on the server
- can call databases and private APIs directly
- never ship JavaScript to the browser
- do not use state or effects
They are ideal for layout, data fetching, and non-interactive UI.
// app/products/page.tsx (Server Component)
import { getProducts } from "@/lib/db";
import { ProductsList } from "./ProductsList";
export default async function ProductsPage() {
const products = await getProducts();
return (
<section>
<h1>Products</h1>
<ProductsList products={products} />
</section>
);
}Here the page loads data on the server and passes plain serialized data to a client component.
Use server components for:
- pages and layouts
- navigation and shells
- dashboards that only display data
- expensive or sensitive logic
2. Client Components for Interaction
Client components run in the browser. They handle:
- state
- event handlers
- effects
- browser APIs like
window,document,localStorage - animations and gesture logic
Mark a file as a client component with "use client".
// app/products/ProductsList.tsx (Client Component)
"use client";
import { useState } from "react";
export function ProductsList({ products }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<button onClick={() => setSelectedId(p.id)}>
{p.name}
</button>
{selectedId === p.id && <span> Selected</span>}
</li>
))}
</ul>
);
}Pattern:
- server component fetches and structures data
- client component adds interactivity on top
Keep client files small and focused. If a component does not need interactivity, keep it on the server.
3. Stable Component Identity
React preserves state based on the component tree structure. A component that is defined inside another component is recreated on every render, which breaks identity and can cause unexpected behavior.
Bad:
function Dashboard() {
// New component function on every render
const Card = ({ title }) => <article>{title}</article>;
return (
<section>
<Card title="Metrics" />
</section>
);
}Good:
function Card({ title }: { title: string }) {
return <article>{title}</article>;
}
function Dashboard() {
return (
<section>
<Card title="Metrics" />
</section>
);
}Rules:
- define components at the module level
- avoid defining components inside render functions
- avoid changing keys unnecessarily
Stable identity keeps state where you expect it and reduces accidental re-renders.
4. Input and Output Component Model
A simple mental model for reusable components:
- Input: props that come in
- Output: events or callbacks that go out
Example: a controlled text input.
interface TextFieldProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export function TextField({ label, value, onChange }: TextFieldProps) {
return (
<label className="field">
<span>{label}</span>
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}Parent controls the state:
const [name, setName] = useState("");
<TextField label="Name" value={name} onChange={setName} />;This pattern keeps components predictable and easy to test.
5. Controlled and Uncontrolled Patterns
React supports two key patterns for form components.
Controlled components
The value lives in React state.
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}Use controlled components when you need:
- validation
- formatting
- real time updates
- integration with global state
Uncontrolled components
The value lives in the DOM. You read it only when needed.
import { useRef } from "react";
function FilePicker({ onSubmit }: { onSubmit: (file: File | null) => void }) {
const fileRef = useRef<HTMLInputElement | null>(null);
return (
<form
onSubmit={(e) => {
e.preventDefault();
const file = fileRef.current?.files?.[0] ?? null;
onSubmit(file);
}}
>
<input type="file" ref={fileRef} />
<button type="submit">Upload</button>
</form>
);
}Use uncontrolled components when:
- you only need the value on submit
- browser behavior is already correct
- you want to avoid re-rendering on every keystroke
6. Composition Instead of Configuration
Configuration heavy components become hard to read and maintain.
Configuration style:
<Card
size="lg"
padding="xl"
showHeader
showFooter
align="center"
>
Content
</Card>Composition style:
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
<Card.Footer>Actions</Card.Footer>
</Card>Composition patterns:
- break complex components into meaningful children
- prefer JSX structure over boolean props
- use slots for flexible layouts
This pattern scales better as requirements change.
7. Headless Logic with Hooks
Modern React encourages separating logic from rendering. Headless hooks encapsulate state and behavior while leaving markup and styling to the caller.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
return {
on,
toggle: () => setOn((v) => !v),
setOn
};
}Usage:
function ToggleButton() {
const { on, toggle } = useToggle();
return (
<button onClick={toggle}>
{on ? "On" : "Off"}
</button>
);
}Same logic can drive different UIs:
function ToggleSwitch() {
const { on, toggle } = useToggle(true);
return (
<div onClick={toggle} className={on ? "switch on" : "switch"}>
{on ? "Enabled" : "Disabled"}
</div>
);
}This keeps component trees clean and makes behaviors reusable across the app.
8. Server First Data Flow
Effect based data fetching used to be common. In modern React, data should load on the server whenever possible.
Old pattern:
// Client component with effect
useEffect(() => {
let ignore = false;
fetch("/api/profile")
.then((res) => res.json())
.then((data) => {
if (!ignore) {
setProfile(data);
}
});
return () => {
ignore = true;
};
}, []);Modern pattern:
// Server component
import { getProfile } from "@/lib/profile";
import { ProfileClient } from "./ProfileClient";
export default async function ProfilePage() {
const profile = await getProfile();
return <ProfileClient profile={profile} />;
}// Client component
"use client";
export function ProfileClient({ profile }) {
const [expanded, setExpanded] = useState(false);
return (
<section>
<h1>{profile.name}</h1>
<button onClick={() => setExpanded((v) => !v)}>
{expanded ? "Hide details" : "Show details"}
</button>
{expanded && <p>{profile.bio}</p>}
</section>
);
}Benefits:
- no race conditions in effects
- easier error handling
- simpler mental model
- less JavaScript in the browser
9. Suspense Boundaries for Async UI
Suspense makes async flows explicit by wrapping an async child in a boundary.
import { Suspense } from "react";
import { UserProfile } from "./UserProfile";
export default function Page() {
return (
<Suspense fallback={<p>Loading profile...</p>}>
<UserProfile />
</Suspense>
);
}UserProfile can use async data fetching with use() in a server context or a Suspense-aware data source.
Patterns:
- place Suspense boundaries around async sections
- keep fallbacks local and descriptive
- combine Suspense with Error Boundaries for full async control
10. Minimizing Effects and Imperative Logic
Effects are one of the most common sources of bugs. Modern React patterns treat effects as a last resort, used only for:
- subscriptions
- connecting to external systems
- browser APIs
- logging and metrics
Prefer:
- server data loading instead of fetch in effects
- derived values instead of state that is kept in sync via effects
- controlled components instead of effect based synchronization
Example of avoiding an unnecessary effect:
Bad:
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);Good:
const total = price * quantity;The second version is easier to reason about and cannot fall out of sync.
Final Thoughts
React 19 encourages a style of development that is closer to pure functions and clear data flow:
- server components handle data and structure
- client components add focused interactivity
- component identity stays stable
- composition replaces configuration
- hooks encapsulate logic
- data loading moves to the server instead of effects
If you adopt these patterns consistently, your React codebase becomes easier to reason about, safer to refactor, and better aligned with how modern React is designed to work.