Learn

/

Prop Organization

Prop Organization

6 patterns

Grouping related props, removing redundancy, and knowing when to extract sub-components.

Group related data props
easy
Avoid
interface ProfileCardProps {
  userName: string;
  userEmail: string;
  userAvatar: string;
  userRole: 'admin' | 'member' | 'viewer';
  onEdit: () => void;
}

Prefer
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.

React Docs: Passing Props
Remove derived props
easy
Avoid
interface ProductListProps {
  products: Product[];
  productCount: number;
  hasProducts: boolean;
  isEmpty: boolean;
  onProductSelect: (product: Product) => void;
}

Prefer
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?

React Docs: Avoid Redundant State
Boolean flag explosion
medium
Avoid
interface ButtonProps {
  children: React.ReactNode;
  isPrimary?: boolean;
  isSecondary?: boolean;
  isDanger?: boolean;
  isOutlined?: boolean;
  isGhost?: boolean;
  isSmall?: boolean;
  isLarge?: boolean;
  onClick?: () => void;
}

Prefer
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.

TypeScript: Union Types
Structure flat coordinate props
medium
Avoid
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;
}

Prefer
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.

React Docs: Passing Props
Extract sub-component via slot
hard
Avoid
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;
}

Prefer
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.

React Docs: Passing JSX as children
Encapsulate internal state
hard
Avoid
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;
}

Prefer
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.

React Docs: Minimal UI State