Sofondo Framework - Patterns and Best Practices
Comprehensive guide to common patterns, best practices, and proven solutions for building with the Sofondo Framework.
Table of Contents
- Component Patterns
- State Management Patterns
- Data Fetching Patterns
- Form Patterns
- Layout Patterns
- Performance Patterns
- Accessibility Patterns
- Error Handling Patterns
- Testing Patterns
Component Patterns
Pattern: Skeleton Loading
Replace content with skeletons while loading.
Problem: Users see empty screens or layout shifts during data loading.
Solution: Use Skeleton components that match your content structure.
import { Skeleton, DataGrid } from '@sofondo/react';
export function ProductList() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
if (loading) {
return (
<div>
{/* Match the structure of your content */}
<Skeleton width="100%" height={60} style={{ marginBottom: '10px' }} />
<Skeleton width="100%" height={50} />
<Skeleton width="100%" height={50} />
<Skeleton width="100%" height={50} />
</div>
);
}
return <DataGrid data={data} columns={columns} keyField="id" />;
}
Benefits:
- No layout shift
- Better perceived performance
- Consistent loading experience
Pattern: Progressive Enhancement
Start with basic functionality, add enhancements progressively.
Problem: Components break in environments without JavaScript or with slow connections.
Solution: Build components that work without JavaScript first.
// Server component - works without JS
import { DataGrid } from '@sofondo/react';
export async function ServerProductList() {
const products = await fetchProducts();
return <DataGrid data={products} columns={columns} keyField="id" />;
}
// Client component - adds interactivity
'use client';
import { useState } from 'react';
export function InteractiveProductList({ initialData }) {
const [data, setData] = useState(initialData);
const [sortBy, setSortBy] = useState('name');
// Client-side sorting and filtering
const sortedData = [...data].sort((a, b) =>
a[sortBy].localeCompare(b[sortBy])
);
return (
<>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<DataGrid data={sortedData} columns={columns} keyField="id" />
</>
);
}
Pattern: Compound Components
Build flexible components with multiple parts.
Problem: Need flexible components without prop explosion.
Solution: Use compound component pattern.
// Instead of this (prop explosion):
<Card
title="Products"
subtitle="Manage inventory"
headerActions={<button>Add</button>}
footer={<div>Footer</div>}
>
Content
</Card>
// Use this (compound pattern):
<Card>
<Card.Header>
<Card.Title>Products</Card.Title>
<Card.Subtitle>Manage inventory</Card.Subtitle>
<Card.Actions>
<button>Add</button>
</Card.Actions>
</Card.Header>
<Card.Body>
Content
</Card.Body>
<Card.Footer>
Footer content
</Card.Footer>
</Card>
State Management Patterns
Pattern: URL State for Filters
Store filter state in URL for shareability and bookmarking.
Problem: Users can’t share or bookmark filtered views.
Solution: Use URL search params for filter state.
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { DataGrid } from '@sofondo/react';
export function ProductList({ products }) {
const searchParams = useSearchParams();
const router = useRouter();
const status = searchParams.get('status') || 'all';
const filteredProducts = status === 'all'
? products
: products.filter(p => p.status === status);
const handleFilterChange = (newStatus: string) => {
const params = new URLSearchParams(searchParams);
params.set('status', newStatus);
router.push(`?${params.toString()}`);
};
return (
<>
<select value={status} onChange={(e) => handleFilterChange(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="draft">Draft</option>
</select>
<DataGrid data={filteredProducts} columns={columns} keyField="id" />
</>
);
}
Benefits:
- Shareable URLs
- Browser back/forward works
- Bookmarkable views
- SEO-friendly
Pattern: Optimistic Updates
Update UI immediately, rollback on error.
Problem: UI feels slow waiting for server responses.
Solution: Update UI optimistically, handle errors gracefully.
'use client';
import { useState } from 'react';
import { useToast } from '@sofondo/react';
export function ProductActions({ product, onUpdate }) {
const [optimisticProduct, setOptimisticProduct] = useState(product);
const { addToast } = useToast();
const handleToggleStatus = async () => {
const previousStatus = optimisticProduct.status;
const newStatus = previousStatus === 'active' ? 'draft' : 'active';
// Update UI immediately
setOptimisticProduct({ ...optimisticProduct, status: newStatus });
addToast(`Product ${newStatus}`, 'success');
try {
// Send to server
await updateProduct(product.id, { status: newStatus });
onUpdate();
} catch (error) {
// Rollback on error
setOptimisticProduct({ ...optimisticProduct, status: previousStatus });
addToast('Failed to update product', 'error');
}
};
return (
<button onClick={handleToggleStatus}>
{optimisticProduct.status === 'active' ? 'Deactivate' : 'Activate'}
</button>
);
}
Pattern: Derived State
Calculate state from other state instead of storing separately.
Problem: State gets out of sync when stored in multiple places.
Solution: Derive state from a single source of truth.
'use client';
import { useState, useMemo } from 'react';
// ❌ Bad: Storing derived state
export function BadProductList() {
const [products, setProducts] = useState([]);
const [activeProducts, setActiveProducts] = useState([]);
const [totalValue, setTotalValue] = useState(0);
// These need manual sync - error-prone!
const addProduct = (product) => {
setProducts([...products, product]);
if (product.status === 'active') {
setActiveProducts([...activeProducts, product]);
}
setTotalValue(totalValue + product.price);
};
}
// ✅ Good: Deriving state
export function GoodProductList() {
const [products, setProducts] = useState([]);
// Derived automatically
const activeProducts = useMemo(
() => products.filter(p => p.status === 'active'),
[products]
);
const totalValue = useMemo(
() => products.reduce((sum, p) => sum + p.price, 0),
[products]
);
const addProduct = (product) => {
setProducts([...products, product]);
// activeProducts and totalValue update automatically
};
}
Data Fetching Patterns
Pattern: Server Components for Initial Data
Fetch data on the server for initial page load.
Problem: Client-side fetching shows loading states and is slower.
Solution: Use Server Components for initial data fetch.
// app/(admin)/products/page.tsx
import { PageHeader, DataGrid } from '@sofondo/react';
// Server Component - no 'use client'
export default async function ProductsPage() {
// Fetch on server
const products = await fetch('https://api.example.com/products').then(r => r.json());
return (
<>
<PageHeader title="Products" />
<DataGrid data={products} columns={columns} keyField="id" />
</>
);
}
Benefits:
- Faster initial load
- No loading spinners
- Better SEO
- Simpler code
Pattern: Streaming with Suspense
Stream data as it loads for better performance.
Problem: Slow API calls block entire page render.
Solution: Use Suspense to stream components.
// app/(admin)/dashboard/page.tsx
import { Suspense } from 'react';
import { Skeleton } from '@sofondo/react';
async function RecentOrders() {
const orders = await fetch('https://api.example.com/orders').then(r => r.json());
return <OrderTable orders={orders} />;
}
async function Analytics() {
const data = await fetch('https://api.example.com/analytics').then(r => r.json());
return <AnalyticsChart data={data} />;
}
export default function Dashboard() {
return (
<>
<PageHeader title="Dashboard" />
{/* Stats load immediately */}
<StatGrid>
<StatCard label="Revenue" value="$12,345" />
</StatGrid>
{/* Orders stream in when ready */}
<Suspense fallback={<Skeleton width="100%" height={200} />}>
<RecentOrders />
</Suspense>
{/* Analytics stream in independently */}
<Suspense fallback={<Skeleton width="100%" height={300} />}>
<Analytics />
</Suspense>
</>
);
}
Pattern: Polling for Real-time Updates
Poll API for updates instead of WebSockets.
Problem: Need real-time updates without WebSocket complexity.
Solution: Use polling with cleanup.
'use client';
import { useState, useEffect } from 'react';
import { DataGrid } from '@sofondo/react';
export function OrderList() {
const [orders, setOrders] = useState([]);
useEffect(() => {
// Initial fetch
fetchOrders().then(setOrders);
// Poll every 30 seconds
const interval = setInterval(() => {
fetchOrders().then(setOrders);
}, 30000);
// Cleanup
return () => clearInterval(interval);
}, []);
return <DataGrid data={orders} columns={columns} keyField="id" />;
}
Form Patterns
Pattern: Controlled Form with Validation
Handle form state and validation with clear feedback.
Problem: Forms need validation without third-party libraries.
Solution: Use controlled inputs with inline validation.
'use client';
import { useState } from 'react';
import { useToast } from '@sofondo/react';
export function ProductForm({ onSubmit }) {
const [formData, setFormData] = useState({
name: '',
price: '',
stock: '',
});
const [errors, setErrors] = useState({});
const { addToast } = useToast();
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.price || parseFloat(formData.price) <= 0) {
newErrors.price = 'Price must be greater than 0';
}
if (!formData.stock || parseInt(formData.stock) < 0) {
newErrors.stock = 'Stock cannot be negative';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) {
addToast('Please fix errors', 'error');
return;
}
try {
await onSubmit(formData);
addToast('Product saved!', 'success');
setFormData({ name: '', price: '', stock: '' });
} catch (error) {
addToast('Failed to save', 'error');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
{errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
</div>
<div>
<label>Price</label>
<input
type="number"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
/>
{errors.price && <span style={{ color: 'red' }}>{errors.price}</span>}
</div>
<div>
<label>Stock</label>
<input
type="number"
value={formData.stock}
onChange={(e) => setFormData({ ...formData, stock: e.target.value })}
/>
{errors.stock && <span style={{ color: 'red' }}>{errors.stock}</span>}
</div>
<button type="submit">Save Product</button>
</form>
);
}
Pattern: Debounced Search
Debounce search input to reduce API calls.
Problem: Search fires too many API requests.
Solution: Use debounce hook.
'use client';
import { useState, useEffect } from 'react';
import { useDebounce } from '@sofondo/react';
import { DataGrid, Skeleton } from '@sofondo/react';
export function ProductSearch() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
// Debounce search term by 500ms
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (!debouncedSearch) {
setResults([]);
return;
}
setLoading(true);
searchProducts(debouncedSearch)
.then(setResults)
.finally(() => setLoading(false));
}, [debouncedSearch]);
return (
<>
<input
type="search"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{loading ? (
<Skeleton width="100%" height={200} />
) : (
<DataGrid data={results} columns={columns} keyField="id" />
)}
</>
);
}
Layout Patterns
Pattern: Responsive Sidebar
Sidebar that adapts to mobile screens.
Problem: Sidebar takes too much space on mobile.
Solution: Auto-collapse on mobile, add toggle.
'use client';
import { useState, useEffect } from 'react';
import { useMediaQuery } from '@sofondo/react';
import { AdminLayout } from '@sofondo/next';
export function ResponsiveLayout({ children }) {
const isMobile = useMediaQuery('(max-width: 768px)');
const [isCollapsed, setIsCollapsed] = useState(false);
// Auto-collapse on mobile
useEffect(() => {
if (isMobile) {
setIsCollapsed(true);
}
}, [isMobile]);
return (
<AdminLayout
menuItems={menuItems}
// AdminLayout handles collapse state internally
>
{children}
</AdminLayout>
);
}
Pattern: Sticky Header Actions
Keep important actions visible while scrolling.
Problem: Users lose context when scrolling long pages.
Solution: Use sticky positioning for headers.
import { PageHeader } from '@sofondo/react';
export function StickyHeaderPage() {
return (
<>
<div style={{ position: 'sticky', top: 0, zIndex: 10, backgroundColor: 'var(--background)' }}>
<PageHeader
title="Products"
actions={<button>Add Product</button>}
/>
</div>
{/* Long content */}
<div style={{ height: '2000px' }}>
Content...
</div>
</>
);
}
Performance Patterns
Pattern: Virtual Scrolling for Large Lists
Render only visible items for better performance.
Problem: Large lists cause performance issues.
Solution: Use virtual scrolling (though DataGrid handles this internally for reasonable sizes).
// For very large datasets (10,000+ items), consider pagination
export function PaginatedList({ items }) {
const [page, setPage] = useState(1);
const pageSize = 50;
const paginatedItems = items.slice(
(page - 1) * pageSize,
page * pageSize
);
return (
<>
<DataGrid data={paginatedItems} columns={columns} keyField="id" />
<div>
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page} of {Math.ceil(items.length / pageSize)}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={page >= Math.ceil(items.length / pageSize)}
>
Next
</button>
</div>
</>
);
}
Pattern: Memoization for Expensive Calculations
Memoize expensive computations.
Problem: Expensive calculations run on every render.
Solution: Use useMemo.
'use client';
import { useMemo } from 'react';
export function Analytics({ orders }) {
// Expensive calculation - only runs when orders change
const analytics = useMemo(() => {
const totalRevenue = orders.reduce((sum, o) => sum + o.amount, 0);
const avgOrderValue = totalRevenue / orders.length;
const topProducts = /* expensive calculation */;
return { totalRevenue, avgOrderValue, topProducts };
}, [orders]);
return (
<div>
<div>Total Revenue: ${analytics.totalRevenue}</div>
<div>Avg Order: ${analytics.avgOrderValue}</div>
</div>
);
}
Accessibility Patterns
Pattern: Keyboard Navigation
Support keyboard navigation in interactive components.
Problem: Users can’t navigate with keyboard.
Solution: Add keyboard event handlers.
'use client';
export function AccessibleMenu({ items, onSelect }) {
const [selectedIndex, setSelectedIndex] = useState(0);
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(i => Math.min(items.length - 1, i + 1));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(i => Math.max(0, i - 1));
break;
case 'Enter':
e.preventDefault();
onSelect(items[selectedIndex]);
break;
}
};
return (
<div
role="menu"
onKeyDown={handleKeyDown}
tabIndex={0}
>
{items.map((item, index) => (
<div
key={item.id}
role="menuitem"
aria-selected={index === selectedIndex}
style={{
backgroundColor: index === selectedIndex ? '#f0f0f0' : 'transparent'
}}
>
{item.label}
</div>
))}
</div>
);
}
Pattern: ARIA Labels for Context
Add ARIA labels for screen readers.
Problem: Screen readers can’t understand icon-only buttons.
Solution: Add aria-label attributes.
export function Actions({ item }) {
return (
<div>
<button
onClick={() => edit(item)}
aria-label={`Edit ${item.name}`}
>
<Edit size={16} />
</button>
<button
onClick={() => delete(item)}
aria-label={`Delete ${item.name}`}
>
<Trash size={16} />
</button>
</div>
);
}
Error Handling Patterns
Pattern: Error Boundaries
Catch errors and show fallback UI.
Problem: Errors crash the entire app.
Solution: Use Error Boundaries.
// components/ErrorBoundary.tsx
'use client';
import { Component } from 'react';
export class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage:
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
Pattern: Graceful Degradation
Handle errors without breaking UI.
Problem: One failed component breaks the page.
Solution: Show partial UI with error state.
'use client';
import { useState, useEffect } from 'react';
import { Skeleton } from '@sofondo/react';
export function ProductStats() {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchStats()
.then(setStats)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) {
return <Skeleton width={200} height={100} />;
}
if (error) {
return (
<div style={{ padding: '20px', backgroundColor: '#fff3cd' }}>
<p>Unable to load stats</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
return <StatCard {...stats} />;
}
Testing Patterns
Pattern: Test Data Builders
Create reusable test data builders.
Problem: Test data is duplicated and hard to maintain.
Solution: Use builder pattern for test data.
// test/builders/productBuilder.ts
export class ProductBuilder {
private product = {
id: 1,
name: 'Test Product',
price: 29.99,
stock: 100,
status: 'active',
};
withName(name: string) {
this.product.name = name;
return this;
}
withPrice(price: number) {
this.product.price = price;
return this;
}
outOfStock() {
this.product.stock = 0;
this.product.status = 'out-of-stock';
return this;
}
build() {
return { ...this.product };
}
}
// Usage in tests:
const product = new ProductBuilder()
.withName('Premium Mouse')
.withPrice(49.99)
.build();
const outOfStockProduct = new ProductBuilder()
.outOfStock()
.build();
Pattern: Component Testing
Test components in isolation.
// __tests__/ProductCard.test.tsx
import { render, screen } from '@testing-library/react';
import { ProductCard } from '../components/ProductCard';
describe('ProductCard', () => {
it('renders product information', () => {
const product = {
id: 1,
name: 'Test Product',
price: 29.99,
};
render(<ProductCard product={product} />);
expect(screen.getByText('Test Product')).toBeInTheDocument();
expect(screen.getByText('$29.99')).toBeInTheDocument();
});
it('shows out of stock badge when stock is 0', () => {
const product = {
id: 1,
name: 'Test Product',
price: 29.99,
stock: 0,
};
render(<ProductCard product={product} />);
expect(screen.getByText('Out of Stock')).toBeInTheDocument();
});
});
Summary
These patterns provide:
✅ Component Patterns - Skeleton loading, progressive enhancement, compound components ✅ State Management - URL state, optimistic updates, derived state ✅ Data Fetching - Server components, streaming, polling ✅ Forms - Validation, debounced search ✅ Layout - Responsive sidebar, sticky headers ✅ Performance - Pagination, memoization ✅ Accessibility - Keyboard navigation, ARIA labels ✅ Error Handling - Error boundaries, graceful degradation ✅ Testing - Test builders, component tests
Further Reading
- Getting Started Guide - Quick start tutorial
- API Reference - Complete API documentation
- Examples - Real-world application examples
- Migration Strategy - Migrate existing projects