SPFx React Components: Reusable Functional Components

SPFx React Components: Reusable Functional Components

Best practices for typing props, defaults, and composition in reusable SPFx components.

Props typing with TypeScript

Define explicit interfaces and prefer readonly props to communicate intent.

type CardProps = {
	readonly title: string;
	readonly subtitle?: string;
	readonly onClick?: () => void;
	readonly children?: React.ReactNode;
};

export function Card({ title, subtitle, onClick, children }: CardProps) {
	return (
		<div className="card" role={onClick ? 'button' : undefined} onClick={onClick}>
			<h3>{title}</h3>
			{subtitle && <p className="text-muted">{subtitle}</p>}
			<div>{children}</div>
		</div>
	);
}

Default and optional props

Provide sensible defaults and avoid overloading components with too many props; split variants when needed.

type BadgeProps = { label: string; intent?: 'info' | 'success' | 'warn' | 'error' };
export function Badge({ label, intent = 'info' }: BadgeProps) {
	return <span className={`badge badge-${intent}`}>{label}</span>;
}

Accessibility basics

Add roles/labels for interactive components, keyboard handlers, and focus management.

type IconButtonProps = { label: string; onClick: () => void };
export function IconButton({ label, onClick }: IconButtonProps) {
	return (
		<button aria-label={label} onClick={onClick}>
			<svg aria-hidden="true">{/* icon */}</svg>
		</button>
	);
}

Reusability patterns

  • Keep components small and focused.
  • Use children-as-API for flexible content.
  • Prefer composition over prop explosion.

Anti‑Patterns

  • Components with large prop surfaces and conditional branches for many modes.
  • Inline anonymous functions recreated every render without useCallback where performance matters.
  • Mixing data fetching directly into presentational components.

SPFx examples

Web part UI building blocks

type WebPartHeaderProps = { title: string; description?: string };
export function WebPartHeader({ title, description }: WebPartHeaderProps) {
	return (
		<header className="wp-header">
			<h2>{title}</h2>
			{description && <p>{description}</p>}
		</header>
	);
}

Memoize hot handlers

export function List({ items, onSelect }: { items: string[]; onSelect: (i: string) => void }) {
	const handleClick = useCallback((i: string) => onSelect(i), [onSelect]);
	return (
		<ul>
			{items.map(i => (
				<li key={i}><button onClick={() => handleClick(i)}>{i}</button></li>
			))}
		</ul>
	);
}