Lesson 18 of 2224 min read

Building Reusable UI Components in Next.js

Learn how to build reusable UI components in Next.js using React's composition model and TypeScript props typing for safer, cleaner components.

Author: CodersNexus

Building Reusable UI Components in Next.js

As a Next.js project grows past a handful of pages, the difference between a codebase that stays maintainable and one that becomes a tangle of copy-pasted markup almost always comes down to one skill: building genuinely reusable UI components. A well-designed Button, Card, or Modal component gets written once and used dozens or hundreds of times across an application, each usage configured slightly differently through props, without duplicating its underlying structure or styling logic.

This lesson focuses on two things that make components truly reusable rather than just technically reused: component composition — the React pattern of building complex UI by combining smaller, focused components together, especially using the children prop — and TypeScript props typing, which makes a component's expected inputs explicit, self-documenting, and safe to use correctly across a large team or codebase.

Learning Objectives

  • Design components with a clear, single responsibility.
  • Use the children prop to build flexible, composable component wrappers.
  • Type component props precisely using TypeScript interfaces or type aliases.
  • Use optional props and sensible defaults to keep components easy to use.
  • Recognize signs that a component should be split into smaller, more reusable pieces.

Core Definitions

  • Component composition: The React pattern of building complex UI by nesting and combining smaller, focused components together, rather than building one large, monolithic component.
  • children prop: A special prop representing whatever JSX is nested between a component's opening and closing tags, letting a component wrap arbitrary content.
  • Props interface: A TypeScript interface or type alias defining the exact shape, names, and types of props a component accepts.
  • Optional prop: A prop marked with a ? in its TypeScript type, meaning it can be omitted by the calling component, typically paired with a default value.
  • Prop drilling: The pattern of passing a prop down through several layers of components that don't need it themselves, purely to reach a deeply nested child that does.

Detailed Explanation

A reusable component starts with a clear, narrow responsibility. A Button component's job is to render a styled, clickable element and respond to interaction — it shouldn't also contain business logic about what happens after it's clicked, or hardcoded text that only makes sense in one specific place it's used. The moment a component starts assuming things about its specific context of use, it stops being reusable.

The children prop is the single most powerful tool for building genuinely flexible, composable components. Rather than a Card component accepting a rigid set of props like title, description, and imageUrl (locking every card into an identical internal structure), a Card component that simply renders {children} inside its styled wrapper can contain literally any content — a form, a chart, a list, plain text — while still consistently providing the padding, border, and shadow styling that makes it recognizably a 'Card' everywhere it's used. This composition pattern — small, focused wrapper components combined with arbitrary nested content — mirrors how HTML itself works (a <div> can contain anything) and is central to how scalable React component libraries are structured.

TypeScript props typing takes reusability a step further by making a component's contract explicit and enforced. Defining a props interface — say, `interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'secondary'; }` — means anyone using this Button component gets immediate, accurate autocomplete and compile-time errors if they forget a required prop, pass the wrong type, or use an invalid value for a restricted option like variant. This transforms a component from something you have to remember how to use correctly (perhaps by reading its source code) into something the type system itself teaches you how to use correctly.

Optional props, marked with a ? in the TypeScript type (like variant?: 'primary' | 'secondary'), let you keep a component simple to use in the common case while still supporting more advanced configuration when needed. Pairing an optional prop with a default value — using JavaScript's default parameter syntax directly in the function signature, like `variant = 'primary'` — means callers who don't care about that option can simply omit it entirely and get sensible default behavior.

As components grow, watch for signs they should be split further: a component accepting an unusually large number of props, a component whose internal JSX has deeply nested conditional rendering for very different use cases, or a component that's copy-pasted and slightly modified in multiple places instead of being genuinely reused. Each of these is a signal that breaking the component into smaller, more focused pieces — composed together rather than configured through an ever-growing prop list — will likely improve both reusability and maintainability.

Diagram Description

Visualize component composition using the children prop:

<Card> ← wrapper: padding, border, shadow
<CardHeader> ← child: consistent header styling
<h2>Order Summary</h2>
</CardHeader>
<CardBody> ← child: consistent body spacing
<p>3 items — ₹1,200 total</p>
</CardBody>
<CardFooter> ← child: consistent footer alignment
<Button label="Checkout" onClick={handleCheckout} />
</CardFooter>
</Card>

Each piece (Card, CardHeader, CardBody, CardFooter, Button) is independently reusable and composed together, rather than one giant 'OrderSummaryCard' component with a huge prop list.

Comparison Table

PatternPurposeExample
children propLet a wrapper component contain arbitrary nested content<Card>{anything}</Card>
Props interfaceMake a component's expected inputs explicit and type-checkedinterface ButtonProps { label: string; ... }
Optional prop + defaultKeep common usage simple while supporting configurationvariant?: 'primary' | 'secondary' = 'primary'
Composition over configurationBuild complex UI from small pieces instead of one prop-heavy component<CardHeader> + <CardBody> instead of <Card title body footer ...>

Next.js Practical Example

// components/Button.tsx — a reusable, precisely typed component
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

export default function Button({
  label,
  onClick,
  variant = 'primary', // sensible default
  disabled = false,
}: ButtonProps) {
  const baseStyles = 'px-4 py-2 rounded-md font-medium';
  const variantStyles =
    variant === 'primary'
      ? 'bg-indigo-600 text-white hover:bg-indigo-700'
      : 'bg-gray-200 text-gray-800 hover:bg-gray-300';

  return (
    <button
      className={`${baseStyles} ${variantStyles}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// components/Card.tsx — a composable wrapper using the children prop
interface CardProps {
  children: React.ReactNode;
}

export default function Card({ children }: CardProps) {
  return <div className="p-6 rounded-lg shadow-md bg-white">{children}</div>;
}

// Usage — composing Card and Button together for a specific case
import Card from '@/components/Card';
import Button from '@/components/Button';

export default function CheckoutSummary() {
  return (
    <Card>
      <h2 className="text-lg font-semibold">Order Summary</h2>
      <p className="text-sm text-gray-500">3 items — ₹1,200 total</p>
      <Button label="Checkout" onClick={() => console.log('checkout')} />
    </Card>
  );
}

Button's ButtonProps interface makes every expected input explicit: label and onClick are required, while variant and disabled are optional, each with a sensible default supplied directly in the function's parameter destructuring. Anyone using Button gets TypeScript-powered autocomplete and an immediate compile error if they forget the required onClick handler or pass an invalid variant value. Card demonstrates composition: rather than accepting rigid props for a title, description, and button, it simply renders {children}, meaning CheckoutSummary can nest a heading, paragraph, and Button — a completely different combination of content than any other place Card might be used — while still getting consistent padding, rounding, and shadow styling from Card itself.

Industry Examples

  • Component libraries like Material UI, Chakra UI, and shadcn/ui are built almost entirely around this composition + typed props pattern, providing small, focused, precisely typed components that combine into complex UI.
  • E-commerce platforms build a single, reusable ProductCard component used identically across search results, category pages, and recommendation carousels, configured only through props rather than being rewritten per page.
  • Design systems at large companies enforce strict TypeScript props interfaces across shared component libraries specifically to prevent inconsistent or incorrect usage as hundreds of engineers build on top of them.
  • SaaS dashboards use composable Card, Table, and Modal wrapper components combined with children to support many different, unpredictable content types within a consistent visual shell.
  • Form libraries commonly expose small, composable Field, Label, and Input components rather than one giant, heavily-configured Form component, letting teams assemble exactly the form layout they need.

Interview Questions and Answers

Q1. What makes a React component genuinely reusable, beyond just being technically reused in multiple places?

A genuinely reusable component has a clear, narrow responsibility, doesn't assume anything about its specific context of use (like hardcoded text or business logic), and is configured through a well-typed set of props rather than requiring modification of its internal source code for each new use case.

Q2. How does the children prop enable component composition?

The children prop represents whatever JSX is nested between a component's opening and closing tags, letting a wrapper component (like a Card) provide consistent styling or structure while containing genuinely arbitrary content, rather than being locked into a fixed, rigid internal layout.

Q3. Why is typing component props with TypeScript valuable for reusability?

A typed props interface makes a component's expected inputs explicit and enforced at compile time, giving other developers accurate autocomplete and immediate errors for missing or incorrect props, effectively turning the type system into living, enforced documentation for how to correctly use the component.

Q4. What is the purpose of an optional prop with a default value?

It lets a component support additional configuration for advanced use cases while keeping the common, default usage simple — callers who don't need that specific option can omit it entirely and the component still behaves sensibly.

Q5. What signals suggest a component should be split into smaller pieces?

An unusually large number of props, deeply nested conditional rendering for very different use cases within a single component, or the same component being copy-pasted and slightly modified in multiple places are all signs that breaking it into smaller, composed pieces would likely improve reusability and maintainability.

MCQs With Answers

1. What does the children prop represent in a React component?

  1. The component's own internal state
  2. Whatever JSX is nested between the component's opening and closing tags
  3. A list of all props passed to the component
  4. The component's CSS class name

Answer: B. Whatever JSX is nested between the component's opening and closing tags

Explanation: children is a special prop automatically populated with any content nested inside a component's JSX tags, enabling flexible composition.

2. Why would you define a TypeScript props interface for a component?

  1. To make the component render faster
  2. To make its expected inputs explicit and enforced at compile time
  3. To automatically generate CSS
  4. To enable server-side rendering

Answer: B. To make its expected inputs explicit and enforced at compile time

Explanation: A props interface documents and enforces exactly what props a component accepts, catching missing or incorrect usage before runtime.

3. What is the purpose of marking a prop as optional with a '?' in TypeScript?

  1. It makes the prop required in production only
  2. It allows the calling component to omit that prop
  3. It disables the prop entirely
  4. It converts the prop to a string type

Answer: B. It allows the calling component to omit that prop

Explanation: A '?' after a prop name in a TypeScript interface marks it as optional, meaning it can be left out by whoever uses the component, often paired with a default value.

4. Which pattern lets a wrapper component like Card contain arbitrary, unpredictable content?

  1. Hardcoding a fixed set of specific props like title and description
  2. Using the children prop
  3. Copy-pasting the component for each use case
  4. Using inline styles only

Answer: B. Using the children prop

Explanation: The children prop lets a component render whatever content is nested inside it, rather than being locked into a fixed, rigid internal structure.

5. What is a warning sign that a component should be split into smaller pieces?

  1. It has zero props
  2. It has an unusually large number of props and deeply nested conditional rendering
  3. It uses the children prop
  4. It has a TypeScript interface

Answer: B. It has an unusually large number of props and deeply nested conditional rendering

Explanation: A bloated prop list and complex internal branching for different use cases often signal that breaking a component into smaller, composed pieces would improve maintainability.

Common Mistakes to Avoid

  • Building components with hardcoded, context-specific content or logic that prevents them from being reused elsewhere.
  • Adding an ever-growing list of props to a single component instead of splitting it into smaller, composable pieces.
  • Skipping TypeScript props typing, leading to components that are easy to misuse without any compile-time warning.
  • Forgetting to provide sensible defaults for optional props, forcing every caller to specify values even for common cases.
  • Overusing prop drilling instead of leveraging composition (passing components as children) to avoid threading props through many uninterested intermediate components.

Interview Notes

  • Reusable components have a narrow, clear responsibility and avoid hardcoded, context-specific assumptions.
  • The children prop enables composition, letting wrapper components contain arbitrary nested content.
  • TypeScript props interfaces make a component's expected inputs explicit and enforced at compile time.
  • Optional props (marked with ?) paired with default values keep common usage simple while supporting advanced configuration.
  • A large prop list or deeply nested conditional rendering are signals a component should be split into smaller, composed pieces.

Key Takeaways

  • True reusability comes from narrow responsibility and flexible composition, not just from a component being used in more than one place.
  • The children prop is the core mechanism enabling flexible, composable wrapper components across the React ecosystem.
  • TypeScript props typing turns a component's usage contract into enforced, self-documenting code rather than tribal knowledge.
  • Recognizing when to split a growing component into smaller, composed pieces is a key skill for keeping a Next.js codebase maintainable at scale.

Summary

Building genuinely reusable UI components in Next.js relies on two complementary skills: component composition and precise TypeScript props typing. Composition, most powerfully enabled by the children prop, lets wrapper components like Card provide consistent styling and structure while containing arbitrary, unpredictable nested content, rather than being locked into a rigid internal layout dictated by a fixed set of props. TypeScript props interfaces make a component's expected inputs explicit and enforced at compile time, giving accurate autocomplete and catching incorrect usage before it ever reaches production, while optional props with sensible defaults keep common usage simple without sacrificing advanced configurability. Recognizing warning signs — an oversized prop list, deeply nested conditional rendering, or components copy-pasted with small modifications — helps identify when splitting a component into smaller, composed pieces will improve both reusability and long-term maintainability.

Frequently Asked Questions

A reusable component has a narrow, clear responsibility and doesn't assume anything specific about its context of use — it's configured entirely through its props, meaning it behaves correctly and usefully across many genuinely different situations, not just copy-pasted with minor edits.

children is a special prop automatically populated with whatever JSX is nested between a component's opening and closing tags. It's important because it enables composition — letting wrapper components like Card or Modal provide consistent structure while containing arbitrary content.

No, reusable components can be built in plain JavaScript, but TypeScript's props typing adds compile-time safety and better autocomplete, making components significantly easier and safer to use correctly across a larger team or codebase.

Add a question mark after the prop name in the interface, like `variant?: 'primary' | 'secondary'`. This allows callers to omit that prop entirely, and it's common practice to pair it with a default value in the component's function signature.

Prop drilling is passing a prop down through several layers of components that don't actually need it themselves, purely to reach a deeply nested child that does. It's considered a maintenance burden because it couples many unrelated components together and makes refactoring harder; composition (passing components as children) can often avoid it.

There's no strict universal number, but if a component's prop list keeps growing to accommodate many different, unrelated use cases, or requires deeply nested conditional logic internally, that's a strong signal it should be split into smaller, composed pieces instead.

Yes, this is very common — for example, a Card component might accept a children prop for its main content while also accepting a specific className prop for additional styling customization, combining configuration and composition.

Configuration means controlling a component's behavior and appearance entirely through a list of props (e.g., title, description, footerText). Composition means building the desired result by combining smaller components together (e.g., nesting a heading and text directly as children). Composition often scales better for genuinely varied content.

Not necessarily — extracting every tiny piece can add unnecessary indirection for content that's truly only used once. The goal is reusability where it provides real value (consistent styling, shared logic, or genuine reuse across multiple places), not extraction for its own sake.

It can add a small amount of upfront time to define the interface, but this is generally outweighed by the errors it catches early, the autocomplete it provides, and the reduced need to read a component's internal source code just to understand how to use it correctly.