Components
Build reusable UI from function components - the only component type you need in modern React.
Search across all documentation pages
Build reusable UI from function components - the only component type you need in modern React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
Badge) composed into larger ones (TaskItem)TaskBoard) owning the state, presentational components receiving it via propsprops object and returns React.ReactNode| 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 |
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;
};// 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 }>;Defining components inside components - Declaring function Inner() {} inside function Outer() {} creates a new component identity every render, destroying Inner's state each time. Fix: Move Inner outside Outer, or memoize with useMemo if 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 key when mapping - React warns and may lose state when items lack keys. Fix: Provide a stable, unique key from 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.
| 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 |
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:
? allow flexible usage. The href prop is optional so the button can either navigate to a specific route or go back in history.label = 'Back to sections') provide sensible fallbacks without requiring a separate defaultProps declaration.(state) => state.startLoading subscribes to only the startLoading action, preventing re-renders when other loading store state changes.interface is preferred over type for props because interfaces produce better error messages and support declaration merging if needed.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.
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.
Use destructuring defaults in the function signature:
function Button({ variant = "primary" }: ButtonProps) {
return <button className={variant}>Click</button>;
}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.
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.
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>
);
}Use React.ComponentPropsWithoutRef<"element">:
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}useState or useReducer)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.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥