Component Design Patterns
React component patterns, composition, and architectural approaches for Smart Shelf components.
Component Design Patterns
Comprehensive guide to React component patterns and architectural approaches used in Smart Shelf for building maintainable, reusable, and scalable UI components.
Core Design Principles
1. Composition over Inheritance
- Build complex UIs from simple, composable components
- Use compound components for related functionality
- Prefer props and children over class inheritance
2. Single Responsibility
- Each component has one clear purpose
- Separate presentation from business logic
- Extract reusable logic into custom hooks
3. Consistency and Reusability
- Standardized component APIs
- Consistent styling and behavior
- Shared component library across the application
Component Pattern Types
1. Compound Components
Components that work together to form a complete UI pattern:
// Example: Alert compound component
export const Alert = {
Root: AlertRoot,
Title: AlertTitle,
Description: AlertDescription,
Actions: AlertActions,
};
// Usage
<Alert.Root variant="destructive">
<Alert.Title>Error</Alert.Title>
<Alert.Description>Something went wrong</Alert.Description>
<Alert.Actions>
<Button onClick={retry}>Retry</Button>
</Alert.Actions>
</Alert.Root>
2. Render Props Pattern
Components that accept functions as children for flexible rendering:
interface AsyncDataProps<T> {
fetcher: () => Promise<T>;
children: (state: {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}) => React.ReactNode;
}
export function AsyncData<T>({ fetcher, children }: AsyncDataProps<T>) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
// Fetch logic...
return children({
...state,
refetch: () => {
setState(prev => ({ ...prev, loading: true }));
// Refetch logic...
},
});
}
// Usage
<AsyncData fetcher={() => fetchProducts()}>
{({ data, loading, error, refetch }) => (
<>
{loading && <Spinner />}
{error && <ErrorMessage error={error} onRetry={refetch} />}
{data && <ProductList products={data} />}
</>
)}
</AsyncData>
3. Custom Hooks Pattern
Extract component logic into reusable hooks:
// Custom hook for inventory management
export function useInventory(options: UseInventoryOptions = {}) {
const [state, setState] = useState<InventoryState>({
items: [],
loading: true,
error: null,
});
const adjustStock = useCallback(async (productId: string, adjustment: StockAdjustment) => {
try {
await inventoryService.adjustStock(productId, adjustment);
// Update local state or refetch
} catch (error) {
setState(prev => ({ ...prev, error }));
}
}, []);
const moveStock = useCallback(async (movement: StockMovement) => {
// Stock movement logic
}, []);
useEffect(() => {
const fetchInventory = async () => {
try {
const items = await inventoryService.getInventory(options);
setState({ items, loading: false, error: null });
} catch (error) {
setState(prev => ({ ...prev, loading: false, error }));
}
};
fetchInventory();
}, [options]);
return {
...state,
adjustStock,
moveStock,
refetch: () => {
setState(prev => ({ ...prev, loading: true }));
// Refetch logic
},
};
}
4. Higher-Order Components (HOCs)
Components that enhance other components with additional functionality:
// Authentication guard HOC
export function withAuth<P extends object>(
Component: React.ComponentType<P>,
requiredRole?: UserRole
) {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useCurrentUser();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/auth/login" replace />;
}
if (requiredRole && !hasRole(user, requiredRole)) {
return <UnauthorizedError />;
}
return <Component {...props} />;
};
}
// Usage
const AdminDashboard = withAuth(DashboardComponent, 'admin');
State Management Patterns
1. Local State with useState
// Simple component state
function ProductSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = useCallback(async (searchQuery: string) => {
const products = await searchProducts(searchQuery);
setResults(products);
}, []);
return (
<div>
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch(query)}
/>
<ProductList products={results} />
</div>
);
}
2. Complex State with useReducer
// Complex form state management
interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
}
type FormAction =
| { type: 'SET_FIELD'; field: string; value: any }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'SET_TOUCHED'; field: string }
| { type: 'SET_SUBMITTING'; isSubmitting: boolean }
| { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' },
};
// Other cases...
default:
return state;
}
}
export function useForm<T>(initialValues: T, validator?: (values: T) => Record<string, string>) {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
touched: {},
isSubmitting: false,
});
// Form methods...
return {
values: state.values,
errors: state.errors,
touched: state.touched,
isSubmitting: state.isSubmitting,
setValue: (field: string, value: any) =>
dispatch({ type: 'SET_FIELD', field, value }),
// Other methods...
};
}
Performance Optimization Patterns
1. Memoization Strategies
// Memoize expensive components
export const ProductList = React.memo(function ProductList({
products,
onSelect
}: ProductListProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={onSelect}
/>
))}
</div>
);
});
// Memoize callbacks to prevent unnecessary re-renders
export function ProductSearch() {
const [query, setQuery] = useState('');
const handleSearch = useCallback(async (searchQuery: string) => {
// Search logic
}, []);
const debouncedSearch = useMemo(
() => debounce(handleSearch, 300),
[handleSearch]
);
return (
<Input
value={query}
onChange={(e) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
}}
/>
);
}
2. Lazy Loading Patterns
// Lazy load heavy components
const DataVisualization = React.lazy(() => import('./DataVisualization'));
const ReportGenerator = React.lazy(() => import('./ReportGenerator'));
export function AnalyticsPage() {
const [activeTab, setActiveTab] = useState('overview');
return (
<div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="charts">Charts</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview">
{/* Always loaded content */}
</TabsContent>
<TabsContent value="charts">
<Suspense fallback={<ChartSkeleton />}>
<DataVisualization />
</Suspense>
</TabsContent>
<TabsContent value="reports">
<Suspense fallback={<ReportSkeleton />}>
<ReportGenerator />
</Suspense>
</TabsContent>
</Tabs>
</div>
);
}
Component Composition Strategies
1. Container and Presentation Pattern
// Presentation Component
interface ProductCardProps {
product: Product;
onEdit: () => void;
onDelete: () => void;
loading?: boolean;
}
function ProductCardPresentation({
product,
onEdit,
onDelete,
loading
}: ProductCardProps) {
return (
<Card className={loading ? 'opacity-50' : ''}>
<CardContent>
<h3>{product.name}</h3>
<p>{product.sku}</p>
<div className="flex gap-2">
<Button onClick={onEdit}>Edit</Button>
<Button variant="destructive" onClick={onDelete}>Delete</Button>
</div>
</CardContent>
</Card>
);
}
// Container Component
export function ProductCard({ productId }: { productId: string }) {
const { product, loading, updateProduct, deleteProduct } = useProduct(productId);
const handleEdit = () => {
// Edit logic
};
const handleDelete = async () => {
await deleteProduct();
};
if (!product) return null;
return (
<ProductCardPresentation
product={product}
onEdit={handleEdit}
onDelete={handleDelete}
loading={loading}
/>
);
}
2. Provider Pattern
// Context Provider
interface InventoryContextValue {
inventory: InventoryItem[];
loading: boolean;
adjustStock: (productId: string, adjustment: StockAdjustment) => Promise<void>;
refreshInventory: () => void;
}
const InventoryContext = createContext<InventoryContextValue | null>(null);
export function InventoryProvider({ children }: { children: React.ReactNode }) {
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [loading, setLoading] = useState(true);
const adjustStock = useCallback(async (productId: string, adjustment: StockAdjustment) => {
// Stock adjustment logic
}, []);
const refreshInventory = useCallback(() => {
// Refresh logic
}, []);
const value = {
inventory,
loading,
adjustStock,
refreshInventory,
};
return (
<InventoryContext.Provider value={value}>
{children}
</InventoryContext.Provider>
);
}
// Hook to use the context
export function useInventoryContext() {
const context = useContext(InventoryContext);
if (!context) {
throw new Error('useInventoryContext must be used within an InventoryProvider');
}
return context;
}
These design patterns ensure maintainable, reusable, and performant React components that scale with the Smart Shelf application.