Compound Components
Compound Components
Pattern with Context
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
function useTabs() {
const context = useContext(TabsContext);
if (!context) throw new Error('useTabs must be used within Tabs');
return context;
}
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
// Usage
<Tabs defaultTab="home">
<TabList>
<Tab id="home">Home</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
</Tabs>
Polymorphic Components
Polymorphic Components
The 'as' Prop Pattern
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
// Usage
type TextProps<C extends React.ElementType> = PolymorphicComponentProp<
C,
{ size?: 'sm' | 'md' | 'lg'; color?: string }
>;
function Text<C extends React.ElementType = 'span'>({
as,
size = 'md',
color,
children,
...rest
}: TextProps<C>) {
const Component = as || 'span';
return (
<Component className={`text-${size}`} style={{ color }} {...rest}>
{children}
</Component>
);
}
// Renders as <span>
<Text size="lg" color="red">Hello</Text>
// Renders as <p>
<Text as="p" size="sm">Paragraph text</Text>
Generic Components
Generic Components
Type-Safe List Component
interface ListProps<T> {
items: T[];
keyExtractor: (item: T) => string;
renderItem: (item: T, index: number) => ReactNode;
emptyMessage?: string;
}
function List<T>({ items, keyExtractor, renderItem, emptyMessage = 'No items' }: ListProps<T>) {
if (items.length === 0) {
return <div className="empty">{emptyMessage}</div>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
interface User {
id: number;
name: string;
}
<List<User>
items={users}
keyExtractor={(user) => String(user.id)}
renderItem={(user) => <span>{user.name}</span>}
/>
Generic Form Component
interface FormProps<T> {
initialValues: T;
onSubmit: (values: T) => void;
validate?: (values: T) => Partial<Record<keyof T, string>>;
children: (props: {
values: T;
errors: Partial<Record<keyof T, string>>;
handleChange: (field: keyof T) => (value: T[keyof T]) => void;
handleSubmit: () => void;
}) => ReactNode;
}
function Form<T extends Record<string, unknown>>({
initialValues,
onSubmit,
validate,
children
}: FormProps<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const handleChange = (field: keyof T) => (value: T[keyof T]) => {
setValues(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = () => {
if (validate) {
const newErrors = validate(values);
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return;
}
onSubmit(values);
};
return <form>{children({ values, errors, handleChange, handleSubmit })}</form>;
}
Higher-Order Components
Higher-Order Components
HOC Pattern
// HOC that adds loading state
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & { isLoading: boolean }
) {
const { isLoading, ...restProps } = props;
if (isLoading) {
return <div className="spinner">Loading...</div>;
}
return <WrappedComponent {...restProps as P} />;
};
}
// Usage
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
// Now accepts isLoading prop
<UserListWithLoading users={users} isLoading={true} />
HOC with Additional Props
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P & { user: User }>
) {
return function WithAuthComponent(props: P) {
const user = useAuth();
if (!user) {
return <Redirect to="/login" />;
}
return <WrappedComponent {...props} user={user} />;
};
}
Best Practices
- Prefer composition over HOCs when possible
- Use custom hooks for logic reuse
- Keep HOCs focused on a single concern
- Use generics to maintain type safety