Components
Build reusable UI from function components — the only component type you need in modern React.
Recipe
Quick-reference recipe card — copy-paste ready.
// Basic component with typed props
interface GreetingProps {
name: string;
className?: string;
}
function Greeting({ name, className }: GreetingProps) {
return <h1 className={className}>Hello, {name}</h1>;
}
// Component with children
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="rounded border p-4">
<h2>{title}</h2>
{children}
</div>
);
}
// Default props via destructuring defaults
function Button({ variant = "primary", ...rest }: ButtonProps) {
return <button className={variant} {...rest} />;
}When to reach for this: Whenever you have a piece of UI that appears more than once, or when a section of JSX is complex enough to deserve its own name.
Working Example
"use client";
import { useState } from "react";
// --- Reusable Primitives ---
interface BadgeProps {
label: string;
color?: "green" | "yellow" | "red";
}
function Badge({ label, color = "green" }: BadgeProps) {
const colors = {
green: "bg-green-100 text-green-800",
yellow: "bg-yellow-100 text-yellow-800",
red: "bg-red-100 text-red-800",
};
return (
<span className={`inline-block rounded px-2 py-0.5 text-xs font-medium ${colors[color]}`}>
{label}
</span>
);
}
// --- Composed Component ---
interface Task {
id: number;
title: string;
status: "done" | "in-progress" | "blocked";
}
const statusColor: Record<Task["status"], BadgeProps["color"]> = {
done: "green",
"in-progress": "yellow",
blocked: "red",
};
function TaskItem({ task }: { task: Task }) {
return (
<li className="flex items-center justify-between border-b py-2">
<span>{task.title}</span>
<Badge label={task.status} color={statusColor[task.status]} />
</li>
);
}
// --- Container Component ---
export function TaskBoard() {
const [tasks] = useState<Task[]>([
{ id: 1, title: "Set up CI pipeline", status: "done" },
{ id: 2, title: "Write integration tests", status: "in-progress" },
{ id: 3, title: "Deploy to production", status: "blocked" },
]);
return (
<section className="max-w-md rounded border p-4">
<h2 className="mb-3 text-lg font-bold">Sprint Board</h2>
<ul>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
</ul>
</section>
);
}What this demonstrates:
- Small, focused components (
Badge) composed into larger ones (TaskItem) - TypeScript interfaces enforcing prop contracts
- Default prop values via destructuring
- Container component (
TaskBoard) owning the state, presentational components receiving it via props
Deep Dive
How It Works
- A function component is any function that accepts a single
propsobject and returnsReact.ReactNode - React calls your function on every render — the function body runs top-to-bottom each time
- Props are read-only — a component never mutates its own props
- Components re-render when their parent re-renders or when their own state or context changes
- React uses the component's position in the tree (not its name) to preserve state between renders
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
props | object | All attributes passed to the component, including children |
props.children | React.ReactNode | Nested JSX passed between opening and closing tags |
| Return | React.ReactNode | JSX, null, string, number, boolean, or an array of these |
Composition Patterns
Slot pattern (named children):
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div className="grid grid-cols-[250px_1fr] grid-rows-[auto_1fr]">
<header className="col-span-2">{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}Render prop (extracting logic):
interface ToggleProps {
children: (isOn: boolean, toggle: () => void) => React.ReactNode;
}
function Toggle({ children }: ToggleProps) {
const [isOn, setIsOn] = useState(false);
return <>{children(isOn, () => setIsOn(prev => !prev))}</>;
}
// Usage
<Toggle>
{(isOn, toggle) => (
<button onClick={toggle}>{isOn ? "ON" : "OFF"}</button>
)}
</Toggle>Compound components:
function Tabs({ children }: { children: React.ReactNode }) {
const [activeIndex, setActiveIndex] = useState(0);
return <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>{children}</TabsContext.Provider>;
}
Tabs.Panel = function Panel({ index, children }: { index: number; children: React.ReactNode }) {
const { activeIndex } = useContext(TabsContext);
return activeIndex === index ? <div>{children}</div> : null;
};TypeScript Notes
// Typing a component that wraps native elements
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}
function Button({ variant = "primary", className, ...rest }: ButtonProps) {
return <button className={`btn-${variant} ${className ?? ""}`} {...rest} />;
}
// React.PropsWithChildren shortcut
type CardProps = React.PropsWithChildren<{ title: string }>;Gotchas
-
Defining components inside components — Declaring
function Inner() {}insidefunction Outer() {}creates a new component identity every render, destroying Inner's state each time. Fix: MoveInneroutsideOuter, or memoize withuseMemoif you truly need closure over parent scope. -
Mutating props —
props.items.push(newItem)mutates the parent's data, leading to subtle bugs. Fix: Treat props as immutable. Create new arrays/objects and lift state changes to the owner. -
Missing
keywhen mapping — React warns and may lose state when items lack keys. Fix: Provide a stable, uniquekeyfrom your data (not array index unless the list is static). -
Over-abstracting too early — Creating
<FlexRow>,<Spacer>, and<Text>for every element adds indirection without reuse payoff. Fix: Wait until you use a pattern three or more times before extracting a component.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Server Components (RSC) | Component only reads data and renders static HTML — no hooks, no event handlers | You need useState, useEffect, or browser APIs |
| Web Components | You need framework-agnostic custom elements shared across React, Vue, etc. | You're fully in a React codebase |
| Template partials (Handlebars) | Server-rendered pages without a JS framework | You need client-side interactivity |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: BackButton with typed props and defaults
// File: src/components/services/back-button.tsx
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { useLoadingStore } from '@/stores/loading-store';
interface BackButtonProps {
href?: string;
label?: string;
}
export default function BackButton({
href,
label = 'Back to sections',
}: BackButtonProps) {
const router = useRouter();
const startLoading = useLoadingStore((state) => state.startLoading);
const handleClick = () => {
startLoading();
if (href) {
router.push(href);
} else {
router.back();
}
};
return (
<button onClick={handleClick} className="flex items-center gap-2 text-sm font-medium cursor-pointer transition-colors">
<ArrowLeft className="h-4 w-4" />
{label}
</button>
);
}What this demonstrates in production:
- Optional props with
?allow flexible usage. Thehrefprop is optional so the button can either navigate to a specific route or go back in history. - Destructuring defaults (
label = 'Back to sections') provide sensible fallbacks without requiring a separatedefaultPropsdeclaration. - Zustand selector
(state) => state.startLoadingsubscribes to only thestartLoadingaction, preventing re-renders when other loading store state changes. interfaceis preferred overtypefor props because interfaces produce better error messages and support declaration merging if needed.- The component is a single responsibility unit: it handles navigation with a loading indicator, nothing more.
FAQs
What is the difference between a component and a regular function?
A component is a function that returns React.ReactNode (JSX, null, string, etc.) and is called by React during rendering. Regular functions are called by your code. Components must start with an uppercase letter so JSX treats them as components, not HTML elements.
Should I use an interface or type for props?
Prefer interface for component props. Interfaces produce better TypeScript error messages and support declaration merging. Use type only when you need union types or mapped types.
How do I set default values for props?
Use destructuring defaults in the function signature:
function Button({ variant = "primary" }: ButtonProps) {
return <button className={variant}>Click</button>;
}What is the children prop and when should I use it?
children is a special prop containing whatever JSX is nested between a component's opening and closing tags. Type it as React.ReactNode. Use it for wrapper/layout components that don't know their content in advance.
Can I define a component inside another component?
Technically yes, but don't. React creates a new component identity each render, destroying all state in the inner component. Always define components at the module level.
What is the slot pattern for components?
Instead of a single children prop, accept multiple named props typed as React.ReactNode:
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}When should I extract a new component?
- When a piece of JSX is used in multiple places
- When a section of a component has its own state or logic
- When a component file exceeds ~150 lines and has clear boundaries
- Don't extract prematurely — wait until you use a pattern at least three times
How do I extend native HTML element props in my component?
Use React.ComponentPropsWithoutRef<"element">:
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}What causes a component to re-render?
- Its parent re-renders
- Its own state changes (via
useStateoruseReducer) - A context it consumes changes
- Props changing alone doesn't trigger a re-render — the parent re-rendering does
What is a container component vs a presentational component?
- Container components own state, fetch data, and handle logic
- Presentational components receive data via props and focus on rendering UI
- This separation keeps components focused and testable, though it's a guideline, not a strict rule
How do React Server Components differ from regular function components?
Server Components run only on the server, can await data directly, have zero client-side JavaScript, and cannot use hooks or event handlers. Regular (client) components run in the browser and support interactivity.
What is React.PropsWithChildren and when do I use it?
A utility type that adds children?: React.ReactNode to your props type:
type CardProps = React.PropsWithChildren<{ title: string }>;Use it when you want a shorthand instead of manually typing children.
Related
- JSX and TSX — the syntax components return
- Conditional Rendering — choosing what a component displays
- Events — making components interactive
- useRef / Forwarding Refs — exposing DOM handles from components