React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

componentspropschildrencomposition

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 props object and returns React.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

ParameterTypeDescription
propsobjectAll attributes passed to the component, including children
props.childrenReact.ReactNodeNested JSX passed between opening and closing tags
ReturnReact.ReactNodeJSX, 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() {} 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 propsprops.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.

Alternatives

AlternativeUse WhenDon't Use When
Server Components (RSC)Component only reads data and renders static HTML — no hooks, no event handlersYou need useState, useEffect, or browser APIs
Web ComponentsYou 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 frameworkYou 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. The href prop 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 separate defaultProps declaration.
  • Zustand selector (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.
  • 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 useState or useReducer)
  • 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.