Rendering Patterns
Conditional rendering, render props, HOCs, memoization, Server Components
Conditional Rendering
Ternaries and Guards
function StatusMessage({
status,
}: {
status: "loading" | "error" | "success"
}) {
return (
<div>
{status === "loading" && <Spinner />}
{status === "error" && <ErrorBanner />}
{status === "success" && <SuccessCheck />}
{/* or ternary for two branches */}
{items.length > 0 ? <ItemList items={items} /> : <EmptyState />}
</div>
)
}
Switch-based rendering
function PaymentStep({ step }: { step: number }) {
switch (step) {
case 1:
return <ShippingForm />
case 2:
return <PaymentForm />
case 3:
return <Confirmation />
default:
return <ShippingForm />
}
}
List Rendering with Keys
interface Item {
id: string
label: string
}
function List({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
)
}
Keys must be stable and unique per list. Index is unsafe when items reorder.
Render Props Pattern
A component that accepts a function as its child to share logic.
"use client"
import { useState, ReactNode } from "react"
interface MouseTrackerRenderProps {
x: number
y: number
}
interface MouseTrackerProps {
children: (state: MouseTrackerRenderProps) => ReactNode
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 })
return (
<div
className="h-40 rounded border"
onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
{children(position)}
</div>
)
}
// Usage
;<MouseTracker>
{({ x, y }) => (
<p className="p-4">
Mouse at ({x}, {y})
</p>
)}
</MouseTracker>
Higher-Order Components (HOC)
A function that takes a component and returns a new component with additional props.
"use client"
import { useState, ComponentType } from "react"
interface WithThemeProps {
theme: "light" | "dark"
toggleTheme: () => void
}
function withTheme<P extends object>(
Component: ComponentType<P & WithThemeProps>
) {
return function ThemedComponent(props: P) {
const [theme, setTheme] = useState<"light" | "dark">("light")
return (
<Component
{...props}
theme={theme}
toggleTheme={() => setTheme(theme === "light" ? "dark" : "light")}
/>
)
}
}
// Usage
function Header({
theme,
toggleTheme,
...props
}: WithThemeProps & { title: string }) {
return (
<header
className={theme === "dark" ? "bg-gray-900 text-white" : "bg-white"}
>
<h1>{props.title}</h1>
<button onClick={toggleTheme}>Toggle theme</button>
</header>
)
}
const ThemedHeader = withTheme(Header)
Controlling Renders
Prevent unnecessary re-renders with memoization.
Server vs Client Components (Mental Model)
Next.js App Router has two component types:
| Server Component (default) | Client Component ("use client") |
|---|---|
| Renders on server, sends HTML | Renders on client (hydrates) |
| Can't use state, effects, browser APIs | Can use hooks, event handlers |
| Can be async (fetch data directly) | Can't be async |
| Smaller bundle (no JS sent) | Sends JS to browser |
// Page.tsx — Server Component (default)
import { db } from "@/lib/db"
export default async function Page() {
const posts = await db.post.findMany()
return <PostList posts={posts} />
}
// PostList.tsx — Client Component
;("use client")
function PostList({ posts }: { posts: Post[] }) {
const [liked, setLiked] = useState<Set<string>>(new Set())
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
<button onClick={() => setLiked((p) => new Set(p).add(post.id))}>
{liked.has(post.id) ? "♥" : "♡"}
</button>
</li>
))}
</ul>
)
}
Rule: default to Server Component. Add "use client" only when you need interactivity.
HOC vs Render Props vs Hooks
| Pattern | Use case |
|---|---|
| Hooks (modern) | Cross-cutting logic shared across components |
| Render Props (legacy) | When consumer needs full rendering control |
| HOC (legacy) | Wrapping multiple components with the same logic |
| Compound Components | Complex coordinated UI (Tabs, Select) |
Today, prefer hooks for sharing stateful logic. Render props and HOCs are still valid for specific cases (layout components, conditionally rendering different wrappers).
See It In Action
1import { useState, type ComponentType } from "react"2 3function withMouseTracking<P>(Wrapped: ComponentType<P & { x: number; y: number }>) {4return function Enhanced(props: P) {5const [pos, setPos] = useState({ x: 0, y: 0 })6return (7 8<div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })} style={{ border: "1px solid", padding: 16 }}>9<p>Mouse: {pos.x}, {pos.y}</p>10<Wrapped {...props} x={pos.x} y={pos.y} />11</div>12)13}14}15 16function Display({ x, y }: { x: number; y: number }) {17return <p>Display: ({x}, {y})</p>18}19 20const MouseTracker = withMouseTracking(Display)withMouseTracking is a Higher-Order Component: it adds mouse tracking state and passes x/y as props to the wrapped component, layering behavior without modifying it.