Sofondo
SofondoFrameworkDocs

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

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>
  );
}

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