Prop Organization
Grouping related props, removing redundancy, and knowing when to extract sub-components.
Group related data props
interface ProfileCardProps {
userName: string;
userEmail: string;
userAvatar: string;
userRole: 'admin' | 'member' | 'viewer';
onEdit: () => void;
}interface User {
name: string;
email: string;
avatar: string;
role: 'admin' | 'member' | 'viewer';
}
interface ProfileCardProps {
user: User;
onEdit: () => void;
}Four user* props that always travel together belong in a User type. The component receives one structured object instead of four loose strings. If User gains a phone field later, only the type changes — not every component that forwards user data.
Remove derived props
interface ProductListProps {
products: Product[];
productCount: number;
hasProducts: boolean;
isEmpty: boolean;
onProductSelect: (product: Product) => void;
}interface ProductListProps {
products: Product[];
onProductSelect: (product: Product) => void;
}
// Derive inside the component:
// const isEmpty = products.length === 0;
// const productCount = products.length;Three of the five props are derivable from products. productCount is products.length, hasProducts is products.length > 0, and isEmpty is its inverse. Redundant props invite bugs: what happens when products has 3 items but isEmpty is true?
Boolean flag explosion
interface ButtonProps {
children: React.ReactNode;
isPrimary?: boolean;
isSecondary?: boolean;
isDanger?: boolean;
isOutlined?: boolean;
isGhost?: boolean;
isSmall?: boolean;
isLarge?: boolean;
onClick?: () => void;
}interface ButtonProps {
children: React.ReactNode;
/** @default 'primary' */
variant?: 'primary' | 'secondary' | 'danger';
/** @default 'filled' */
appearance?: 'filled' | 'outlined' | 'ghost';
/** @default 'md' */
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
}Seven booleans collapsed into three union props. Unions enforce mutual exclusivity — a button can't be both primary and danger. Booleans allow impossible combos like <Button isPrimary isDanger isOutlined isGhost />. Each union prop represents one independent design axis.
Structure flat coordinate props
interface Coordinates {
x: number;
y: number;
}
interface MapViewProps {
center: Coordinates;
zoom: number;
zoomRange?: { min: number; max: number };
marker?: { coords: Coordinates; text: string };
onMapClick: (coords: Coordinates) => void;
}interface LatLng {
lat: number;
lng: number;
}
interface MapViewProps {
center: LatLng;
zoom: number;
zoomRange?: { min: number; max: number };
marker?: { position: LatLng; label: string };
onMapClick: (position: LatLng) => void;
}Domain-specific names beat generic ones: LatLng with lat/lng is immediately clear for a map, while Coordinates with x/y could mean screen pixels, grid positions, or anything.
position tells you where the marker sits; coords is just a synonym for the type name. label specifies what gets displayed; text is vague.
Extract sub-component via slot
interface ArticlePageProps {
title: string;
content: string;
author: string;
publishedAt: Date;
toolbarPosition?: 'top' | 'bottom';
isShareVisible?: boolean;
isBookmarkVisible?: boolean;
isPrintVisible?: boolean;
onShare?: () => void;
onBookmark?: () => void;
onPrint?: () => void;
}interface ArticlePageProps {
title: string;
content: string;
author: string;
publishedAt: Date;
/** Toolbar rendered above the content. */
toolbar?: React.ReactNode;
}
// Usage:
// <ArticlePage title="..." content="..."
// toolbar={
// <ArticleToolbar
// onShare={handleShare}
// onBookmark={handleBookmark}
// />
// }
// />Six out of eleven original props belonged to the toolbar, not the article. Extracting the toolbar into a ReactNode slot cuts the interface in half.
Consumers compose their own toolbar — or omit it entirely. The ArticlePage no longer needs to know what toolbar actions exist.
Encapsulate internal state
interface SearchInputProps {
query: string;
onQueryChange: (query: string) => void;
placeholder?: string;
results: SearchResult[];
onResultSelect: (result: SearchResult) => void;
isDropdownOpen: boolean;
onDropdownToggle: () => void;
highlightedIndex: number;
onHighlightChange: (index: number) => void;
isLoading?: boolean;
}interface SearchInputProps {
query: string;
onQueryChange: (query: string) => void;
placeholder?: string;
results: SearchResult[];
onResultSelect: (result: SearchResult) => void;
isLoading?: boolean;
}
// Dropdown open state, highlighted index, and
// keyboard navigation are managed internally.
// The parent controls data and selection only.Not every piece of state needs to be a prop. Dropdown visibility and keyboard-highlighted index are UI interaction details — the parent doesn't care which item is highlighted.
Exposing internal state as props forces the parent to reimplement dropdown behavior. Keep the API to what the parent actually needs: data in, selection out.