Complete React component patterns system. PROACTIVELY activate for: (1) Compound components with context, (2) Render props pattern, (3) Higher-Order Components (HOC), (4) Custom hooks as patterns, (5) Provider pattern with reducer, (6) Controlled vs uncontrolled components, (7) Prop getter pattern, (8) State reducer pattern. Provides: Pattern implementations, composition strategies, reusable component APIs, flexible state control. Ensures clean, maintainable component architecture.
/plugin marketplace add JosiahSiegel/claude-plugin-marketplace/plugin install react-master@claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
| Pattern | Use Case | Example |
|---|---|---|
| Compound Components | Related components sharing state | <Tabs><Tab /><Panel /></Tabs> |
| Render Props | Dynamic rendering with shared logic | <Mouse>{({x,y}) => ...}</Mouse> |
| HOC | Cross-cutting concerns | withAuth(Component) |
| Custom Hooks | Stateful logic reuse | useToggle(), useDebounce() |
| Provider Pattern | Global/shared state | <CartProvider>...</CartProvider> |
| Controlled/Uncontrolled | Form input flexibility | value vs defaultValue |
| Prop Getters | Accessible component APIs | getButtonProps() |
| State Reducer | Customizable state logic | useReducer(customReducer) |
Use for React component architecture:
For state management: see react-state-management
import { createContext, useContext, useState, ReactNode } from 'react';
// Context for sharing state
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error('Tabs components must be used within a Tabs provider');
}
return context;
}
// Parent component
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
function Tabs({ defaultTab, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Tab List
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
// Tab Button
interface TabProps {
value: string;
children: ReactNode;
}
function Tab({ value, children }: TabProps) {
const { activeTab, setActiveTab } = useTabsContext();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
className={activeTab === value ? 'active' : ''}
>
{children}
</button>
);
}
// Tab Panel
interface TabPanelProps {
value: string;
children: ReactNode;
}
function TabPanel({ value, children }: TabPanelProps) {
const { activeTab } = useTabsContext();
if (activeTab !== value) return null;
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
);
}
// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
export { Tabs };
// Usage
function App() {
return (
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="features">Features</Tabs.Tab>
<Tabs.Tab value="pricing">Pricing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="features">Features content</Tabs.Panel>
<Tabs.Panel value="pricing">Pricing content</Tabs.Panel>
</Tabs>
);
}
import { createContext, useContext, useState, ReactNode, isValidElement, Children, cloneElement } from 'react';
interface AccordionContextType {
openItems: Set<string>;
toggleItem: (id: string) => void;
allowMultiple: boolean;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
interface AccordionProps {
children: ReactNode;
allowMultiple?: boolean;
defaultOpen?: string[];
}
function Accordion({ children, allowMultiple = false, defaultOpen = [] }: AccordionProps) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));
const toggleItem = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) next.clear();
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem, allowMultiple }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
interface AccordionItemProps {
id: string;
children: ReactNode;
}
function AccordionItem({ id, children }: AccordionItemProps) {
const context = useContext(AccordionContext);
if (!context) throw new Error('AccordionItem must be within Accordion');
const isOpen = context.openItems.has(id);
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`} data-state={isOpen ? 'open' : 'closed'}>
{Children.map(children, (child) => {
if (isValidElement(child)) {
return cloneElement(child as React.ReactElement<{ itemId?: string; isOpen?: boolean }>, {
itemId: id,
isOpen,
});
}
return child;
})}
</div>
);
}
interface AccordionTriggerProps {
children: ReactNode;
itemId?: string;
isOpen?: boolean;
}
function AccordionTrigger({ children, itemId, isOpen }: AccordionTriggerProps) {
const context = useContext(AccordionContext);
if (!context || !itemId) return null;
return (
<button
className="accordion-trigger"
aria-expanded={isOpen}
onClick={() => context.toggleItem(itemId)}
>
{children}
<span className="icon">{isOpen ? '−' : '+'}</span>
</button>
);
}
interface AccordionContentProps {
children: ReactNode;
isOpen?: boolean;
}
function AccordionContent({ children, isOpen }: AccordionContentProps) {
if (!isOpen) return null;
return <div className="accordion-content">{children}</div>;
}
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
export { Accordion };
import { useState, ReactNode } from 'react';
interface ToggleRenderProps {
on: boolean;
toggle: () => void;
setOn: (value: boolean) => void;
}
interface ToggleProps {
defaultOn?: boolean;
children: (props: ToggleRenderProps) => ReactNode;
}
function Toggle({ defaultOn = false, children }: ToggleProps) {
const [on, setOn] = useState(defaultOn);
const toggle = () => setOn((prev) => !prev);
return <>{children({ on, toggle, setOn })}</>;
}
// Usage
function App() {
return (
<Toggle defaultOn={false}>
{({ on, toggle }) => (
<div>
<p>The toggle is {on ? 'ON' : 'OFF'}</p>
<button onClick={toggle}>Toggle</button>
</div>
)}
</Toggle>
);
}
import { useState, useEffect, ReactNode } from 'react';
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
children: (position: MousePosition) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return <>{children(position)}</>;
}
// Usage
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<div
style={{
position: 'absolute',
left: x + 10,
top: y + 10,
background: 'black',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
}}
>
{x}, {y}
</div>
)}
</MouseTracker>
);
}
import { useState, useEffect, ReactNode } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
interface FetchProps<T> {
url: string;
children: (state: FetchState<T>) => ReactNode;
}
function Fetch<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return <>{children({ data, loading, error, refetch: fetchData })}</>;
}
// Usage
function UserList() {
return (
<Fetch<User[]> url="/api/users">
{({ data, loading, error, refetch }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}}
</Fetch>
);
}
import { ComponentType } from 'react';
interface WithLoadingProps {
isLoading: boolean;
}
function withLoading<P extends object>(
WrappedComponent: ComponentType<P>
) {
return function WithLoadingComponent(props: P & WithLoadingProps) {
const { isLoading, ...rest } = props;
if (isLoading) {
return <div className="loading-spinner">Loading...</div>;
}
return <WrappedComponent {...(rest as P)} />;
};
}
// Usage
interface UserListProps {
users: User[];
}
function UserList({ users }: UserListProps) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
// In parent component
<UserListWithLoading isLoading={loading} users={users} />
import { ComponentType, useEffect, useState } from 'react';
interface User {
id: string;
name: string;
email: string;
}
interface WithUserProps {
user: User | null;
userLoading: boolean;
}
function withUser<P extends WithUserProps>(
WrappedComponent: ComponentType<P>
) {
return function WithUserComponent(
props: Omit<P, keyof WithUserProps>
) {
const [user, setUser] = useState<User | null>(null);
const [userLoading, setUserLoading] = useState(true);
useEffect(() => {
fetch('/api/me')
.then((res) => res.json())
.then((data) => {
setUser(data);
setUserLoading(false);
})
.catch(() => setUserLoading(false));
}, []);
return (
<WrappedComponent
{...(props as P)}
user={user}
userLoading={userLoading}
/>
);
};
}
// Usage
interface ProfileProps extends WithUserProps {
showAvatar?: boolean;
}
function Profile({ user, userLoading, showAvatar = true }: ProfileProps) {
if (userLoading) return <div>Loading user...</div>;
if (!user) return <div>Not logged in</div>;
return (
<div>
{showAvatar && <img src={`/avatars/${user.id}`} alt="" />}
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
const ProfileWithUser = withUser(Profile);
import { ComponentType } from 'react';
// Utility for composing HOCs
function compose<P>(...hocs: Array<(c: ComponentType<any>) => ComponentType<any>>) {
return (Component: ComponentType<P>) =>
hocs.reduceRight((acc, hoc) => hoc(acc), Component);
}
// Usage
const EnhancedComponent = compose(
withLoading,
withUser,
withTheme
)(BaseComponent);
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse, setValue };
}
// Usage
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal">
<button onClick={close}>Close</button>
</div>
)}
</>
);
}
import { useState, useCallback } from 'react';
interface UseDisclosureReturn {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
onToggle: () => void;
getDisclosureProps: () => { 'aria-expanded': boolean; 'aria-controls': string };
getContentProps: () => { id: string; hidden: boolean };
}
function useDisclosure(
id: string,
{ defaultOpen = false }: { defaultOpen?: boolean } = {}
): UseDisclosureReturn {
const [isOpen, setIsOpen] = useState(defaultOpen);
const onOpen = useCallback(() => setIsOpen(true), []);
const onClose = useCallback(() => setIsOpen(false), []);
const onToggle = useCallback(() => setIsOpen((prev) => !prev), []);
const getDisclosureProps = useCallback(
() => ({
'aria-expanded': isOpen,
'aria-controls': `${id}-content`,
}),
[isOpen, id]
);
const getContentProps = useCallback(
() => ({
id: `${id}-content`,
hidden: !isOpen,
}),
[isOpen, id]
);
return {
isOpen,
onOpen,
onClose,
onToggle,
getDisclosureProps,
getContentProps,
};
}
import { useEffect, useRef, RefObject } from 'react';
function useClickOutside<T extends HTMLElement>(
handler: () => void,
events: Array<'mousedown' | 'mouseup' | 'touchstart' | 'touchend'> = ['mousedown', 'touchstart']
): RefObject<T> {
const ref = useRef<T>(null);
useEffect(() => {
const listener = (event: Event) => {
const target = event.target as Node;
if (!ref.current || ref.current.contains(target)) {
return;
}
handler();
};
events.forEach((event) => document.addEventListener(event, listener));
return () => {
events.forEach((event) => document.removeEventListener(event, listener));
};
}, [handler, events]);
return ref;
}
// Usage
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
return (
<div ref={ref}>
<button onClick={() => setIsOpen(true)}>Open</button>
{isOpen && <div className="dropdown-menu">Menu content</div>}
</div>
);
}
import { createContext, useContext, useReducer, ReactNode, Dispatch } from 'react';
// Types
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' };
// Reducer
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find((item) => item.id === action.payload.id);
if (existingItem) {
const items = state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
return { items, total: calculateTotal(items) };
}
const items = [...state.items, { ...action.payload, quantity: 1 }];
return { items, total: calculateTotal(items) };
}
case 'REMOVE_ITEM': {
const items = state.items.filter((item) => item.id !== action.payload);
return { items, total: calculateTotal(items) };
}
case 'UPDATE_QUANTITY': {
const items = state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
return { items, total: calculateTotal(items) };
}
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Context
interface CartContextType {
state: CartState;
dispatch: Dispatch<CartAction>;
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
}
const CartContext = createContext<CartContextType | null>(null);
// Provider
function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
const addItem = (item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
return (
<CartContext.Provider
value={{ state, dispatch, addItem, removeItem, updateQuantity, clearCart }}
>
{children}
</CartContext.Provider>
);
}
// Hook
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
export { CartProvider, useCart };
import { useState, ChangeEvent } from 'react';
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return (
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Controlled input"
/>
);
}
import { useRef, FormEvent } from 'react';
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log('Value:', inputRef.current?.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}
import { useState, useCallback, ChangeEvent } from 'react';
interface InputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
function Input({ value: controlledValue, defaultValue = '', onChange }: InputProps) {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
if (!isControlled) {
setInternalValue(newValue);
}
onChange?.(newValue);
},
[isControlled, onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
}
// Usage - Controlled
function ControlledUsage() {
const [value, setValue] = useState('');
return <Input value={value} onChange={setValue} />;
}
// Usage - Uncontrolled
function UncontrolledUsage() {
return <Input defaultValue="initial" onChange={console.log} />;
}
import { useState, useCallback, HTMLAttributes, InputHTMLAttributes } from 'react';
interface UseSelectReturn<T> {
selectedItem: T | null;
isOpen: boolean;
highlightedIndex: number;
getToggleButtonProps: () => HTMLAttributes<HTMLButtonElement>;
getMenuProps: () => HTMLAttributes<HTMLUListElement>;
getItemProps: (options: { item: T; index: number }) => HTMLAttributes<HTMLLIElement>;
getInputProps: () => InputHTMLAttributes<HTMLInputElement>;
}
function useSelect<T extends { id: string; label: string }>(
items: T[]
): UseSelectReturn<T> {
const [selectedItem, setSelectedItem] = useState<T | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const getToggleButtonProps = useCallback(
() => ({
onClick: () => setIsOpen((prev) => !prev),
'aria-haspopup': 'listbox' as const,
'aria-expanded': isOpen,
}),
[isOpen]
);
const getMenuProps = useCallback(
() => ({
role: 'listbox' as const,
'aria-activedescendant': highlightedIndex >= 0 ? items[highlightedIndex]?.id : undefined,
hidden: !isOpen,
}),
[isOpen, highlightedIndex, items]
);
const getItemProps = useCallback(
({ item, index }: { item: T; index: number }) => ({
role: 'option' as const,
id: item.id,
'aria-selected': selectedItem?.id === item.id,
onClick: () => {
setSelectedItem(item);
setIsOpen(false);
},
onMouseEnter: () => setHighlightedIndex(index),
}),
[selectedItem]
);
const getInputProps = useCallback(
() => ({
value: selectedItem?.label || '',
readOnly: true,
onClick: () => setIsOpen(true),
}),
[selectedItem]
);
return {
selectedItem,
isOpen,
highlightedIndex,
getToggleButtonProps,
getMenuProps,
getItemProps,
getInputProps,
};
}
// Usage
function Select({ items }: { items: Array<{ id: string; label: string }> }) {
const {
selectedItem,
isOpen,
highlightedIndex,
getToggleButtonProps,
getMenuProps,
getItemProps,
getInputProps,
} = useSelect(items);
return (
<div className="select">
<input {...getInputProps()} placeholder="Select an item" />
<button {...getToggleButtonProps()}>▼</button>
<ul {...getMenuProps()}>
{isOpen &&
items.map((item, index) => (
<li
key={item.id}
{...getItemProps({ item, index })}
className={highlightedIndex === index ? 'highlighted' : ''}
>
{item.label}
</li>
))}
</ul>
</div>
);
}
import { useReducer, Reducer } from 'react';
// Types
type ToggleState = { on: boolean };
type ToggleAction = { type: 'toggle' } | { type: 'reset'; initialState: ToggleState };
// Default reducer
function toggleReducer(state: ToggleState, action: ToggleAction): ToggleState {
switch (action.type) {
case 'toggle':
return { on: !state.on };
case 'reset':
return action.initialState;
default:
return state;
}
}
interface UseToggleOptions {
initialOn?: boolean;
reducer?: Reducer<ToggleState, ToggleAction>;
}
function useToggleWithReducer({
initialOn = false,
reducer = toggleReducer,
}: UseToggleOptions = {}) {
const initialState = { on: initialOn };
const [state, dispatch] = useReducer(reducer, initialState);
const toggle = () => dispatch({ type: 'toggle' });
const reset = () => dispatch({ type: 'reset', initialState });
return { ...state, toggle, reset, dispatch };
}
// Usage with custom reducer
function App() {
const customReducer: Reducer<ToggleState, ToggleAction> = (state, action) => {
// Custom logic: can only toggle if some condition
if (action.type === 'toggle' && state.on) {
// Prevent turning off
return state;
}
return toggleReducer(state, action);
};
const { on, toggle } = useToggleWithReducer({ reducer: customReducer });
return (
<button onClick={toggle}>
{on ? 'ON (cannot turn off)' : 'OFF'}
</button>
);
}
| Pattern | Use Case |
|---|---|
| Compound Components | Related components sharing implicit state |
| Render Props | Dynamic rendering with shared logic |
| HOC | Cross-cutting concerns, code reuse |
| Custom Hooks | Stateful logic reuse |
| Provider Pattern | Global/shared state management |
| Controlled/Uncontrolled | Form input flexibility |
| Prop Getters | Accessible component APIs |
| State Reducer | Customizable state logic |
Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.