Files
MyClub/frontend/FRONTEND_UTILITIES_GUIDE.md
T
Tomas Dvorak 9ccca365b3 dev day #65
2025-10-19 18:09:28 +02:00

16 KiB

Frontend Utility Hooks & Components Guide

Complete TypeScript/TSX utilities that mirror the backend helpers and make frontend development much easier.

Table of Contents

  1. Hooks
  2. Components
  3. Utilities
  4. Complete Example

Hooks

usePaginatedData

Purpose: Fetch paginated data with search, sort, and filters in one line.

Features:

  • Automatic pagination management
  • Search functionality
  • Sorting
  • Filtering
  • Loading and error states
  • Works seamlessly with backend QueryParser
import { usePaginatedData } from '../hooks/usePaginatedData';

interface Article {
  id: number;
  title: string;
  published: boolean;
}

function ArticleList() {
  const {
    data: articles,
    meta,
    loading,
    error,
    setPage,
    setSearch,
    setSort,
    setFilters,
    refresh,
  } = usePaginatedData<Article>('/articles', {
    page_size: 20,
  });

  return (
    <div>
      <input 
        type="text" 
        onChange={(e) => setSearch(e.target.value)} 
        placeholder="Search..." 
      />
      
      <select onChange={(e) => setFilters({ published: e.target.value })}>
        <option value="">All</option>
        <option value="true">Published</option>
        <option value="false">Draft</option>
      </select>

      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      
      <ul>
        {articles.map(article => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>

      {meta && (
        <button 
          onClick={() => setPage(meta.page + 1)} 
          disabled={!meta.has_next}
        >
          Next Page
        </button>
      )}
    </div>
  );
}

useApiMutation

Purpose: Handle POST, PUT, PATCH, DELETE requests with loading states.

Features:

  • Automatic loading states
  • Error handling
  • Success tracking
  • TypeScript support
import { useApiPost, useApiDelete } from '../hooks/useApiMutation';

function ArticleForm() {
  // Create article
  const { mutate: createArticle, loading, error, success } = useApiPost<Article>('/articles');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const article = await createArticle({
      title: 'New Article',
      content: 'Content here...',
    });
    
    if (article) {
      console.log('Created:', article);
    }
  };

  // Delete article
  const deleteArticle = useApiDelete((data: { id: number }) => `/articles/${data.id}`);

  const handleDelete = async (id: number) => {
    await deleteArticle.mutate({ id });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Article'}
      </button>
      {error && <p className="error">{error}</p>}
      {success && <p className="success">Article created!</p>}
    </form>
  );
}

useFormValidation

Purpose: Form validation with built-in rules and error messages.

Features:

  • Required, min/max length, pattern, email, URL validators
  • Custom validators
  • Automatic error messages
  • Touch tracking
  • Easy integration with forms
import { useFormValidation } from '../hooks/useFormValidation';

interface ArticleForm {
  title: string;
  content: string;
  email: string;
  url: string;
}

function CreateArticleForm() {
  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
  } = useFormValidation<ArticleForm>(
    {
      title: '',
      content: '',
      email: '',
      url: '',
    },
    {
      title: { required: true, min: 3, max: 200 },
      content: { required: true, min: 10 },
      email: { required: true, email: true },
      url: { url: true },
    }
  );

  const onSubmit = async (data: ArticleForm) => {
    console.log('Valid data:', data);
    // Call API here
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          type="text"
          name="title"
          value={values.title}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Title"
        />
        {touched.title && errors.title && (
          <span className="error">{errors.title}</span>
        )}
      </div>

      <div>
        <textarea
          name="content"
          value={values.content}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Content"
        />
        {touched.content && errors.content && (
          <span className="error">{errors.content}</span>
        )}
      </div>

      <button type="submit">Create Article</button>
    </form>
  );
}

useQueryBuilder

Purpose: Build query strings for API calls with filters, search, and sort.

Features:

  • Filter management
  • Search management
  • Sort management
  • Pagination
  • URL-friendly query string generation
import { useQueryBuilder } from '../hooks/useQueryBuilder';

function ArticleFilters() {
  const query = useQueryBuilder(20);

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        onChange={(e) => query.setSearch(e.target.value)}
      />

      <select onChange={(e) => query.setFilter('published', e.target.value)}>
        <option value="">All</option>
        <option value="true">Published</option>
        <option value="false">Draft</option>
      </select>

      <button onClick={() => query.setSort('created_at', 'desc')}>
        Sort by Date
      </button>

      <p>Query: {query.queryString}</p>
      {/* Use in API call: `/articles?${query.queryString}` */}
    </div>
  );
}

useToast

Purpose: Display toast notifications for user feedback.

Features:

  • Success, error, warning, info types
  • Auto-dismiss
  • Manual dismiss
  • Queue management
import { useToast } from '../hooks/useToast';
import { ToastContainer } from '../components/common/ToastContainer';

function App() {
  const toast = useToast();

  const handleSave = async () => {
    try {
      // Save logic
      toast.success('Article saved successfully!');
    } catch (error) {
      toast.error('Failed to save article');
    }
  };

  return (
    <div>
      <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
      
      <button onClick={handleSave}>Save</button>
      <button onClick={() => toast.info('This is info')}>Show Info</button>
      <button onClick={() => toast.warning('Warning!')}>Show Warning</button>
    </div>
  );
}

useBatchSelection

Purpose: Manage selection of multiple items in tables/lists.

Features:

  • Select/deselect individual items
  • Select all / deselect all
  • Track selected items
  • Get selected IDs or full items
import { useBatchSelection } from '../hooks/useBatchSelection';

function ArticleTable() {
  const articles = [
    { id: 1, title: 'Article 1' },
    { id: 2, title: 'Article 2' },
    { id: 3, title: 'Article 3' },
  ];

  const selection = useBatchSelection(articles, 'id');

  const handleBatchDelete = () => {
    const ids = selection.getSelectedIds();
    console.log('Delete articles:', ids);
    // Call batch delete API
  };

  return (
    <div>
      <div>
        <input
          type="checkbox"
          checked={selection.isAllSelected}
          onChange={selection.toggleAll}
        />
        <label>Select All</label>
      </div>

      {articles.map((article) => (
        <div key={article.id}>
          <input
            type="checkbox"
            checked={selection.isSelected(article.id)}
            onChange={() => selection.toggle(article.id)}
          />
          <span>{article.title}</span>
        </div>
      ))}

      {selection.selectedIds.size > 0 && (
        <button onClick={handleBatchDelete}>
          Delete {selection.selectedIds.size} selected
        </button>
      )}
    </div>
  );
}

Components

DataTable

Purpose: Feature-rich data table with sorting, selection, and custom rendering.

Features:

  • Sortable columns
  • Row selection (single or multiple)
  • Custom cell rendering
  • Actions column
  • Loading and empty states
  • Responsive design
import { DataTable, Column } from '../components/common/DataTable';

interface Article {
  id: number;
  title: string;
  published: boolean;
  created_at: string;
}

function ArticleTable() {
  const articles: Article[] = [/* ... */];
  const selection = useBatchSelection(articles, 'id');

  const columns: Column<Article>[] = [
    {
      key: 'id',
      label: 'ID',
      sortable: true,
      width: '80px',
    },
    {
      key: 'title',
      label: 'Title',
      sortable: true,
      render: (article) => (
        <strong>{article.title}</strong>
      ),
    },
    {
      key: 'published',
      label: 'Status',
      render: (article) => (
        <span className={article.published ? 'published' : 'draft'}>
          {article.published ? 'Published' : 'Draft'}
        </span>
      ),
    },
    {
      key: 'created_at',
      label: 'Created',
      sortable: true,
      render: (article) => new Date(article.created_at).toLocaleDateString(),
    },
  ];

  return (
    <DataTable
      data={articles}
      columns={columns}
      selectable
      selectedIds={selection.selectedIds}
      onToggleSelect={selection.toggle}
      onToggleSelectAll={selection.toggleAll}
      isAllSelected={selection.isAllSelected}
      isSomeSelected={selection.isSomeSelected}
      onSort={(field) => console.log('Sort by:', field)}
      actions={(article) => (
        <div>
          <button onClick={() => console.log('Edit', article.id)}>Edit</button>
          <button onClick={() => console.log('Delete', article.id)}>Delete</button>
        </div>
      )}
    />
  );
}

ToastContainer

Purpose: Display toast notifications from useToast hook.

import { useToast } from '../hooks/useToast';
import { ToastContainer } from '../components/common/ToastContainer';

function App() {
  const toast = useToast();

  return (
    <div>
      <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
      {/* Your app content */}
    </div>
  );
}

Utilities

Export Functions

Purpose: Export data to CSV/JSON files.

import {
  exportToCSV,
  exportToJSON,
  exportFromAPI,
  getExportFilename,
  copyToClipboard,
} from '../utils/export';

function ExportButtons() {
  const articles = [
    { id: 1, title: 'Article 1', published: true },
    { id: 2, title: 'Article 2', published: false },
  ];

  // Export to CSV
  const handleExportCSV = () => {
    const filename = getExportFilename('articles', 'csv');
    exportToCSV(articles, filename);
  };

  // Export to JSON
  const handleExportJSON = () => {
    const filename = getExportFilename('articles', 'json');
    exportToJSON(articles, filename);
  };

  // Export from API endpoint
  const handleExportFromAPI = async () => {
    try {
      await exportFromAPI('/articles/export', 'articles.csv', 'csv');
    } catch (error) {
      console.error('Export failed');
    }
  };

  // Copy to clipboard
  const handleCopy = async () => {
    const success = await copyToClipboard(JSON.stringify(articles, null, 2));
    if (success) {
      alert('Copied to clipboard!');
    }
  };

  return (
    <div>
      <button onClick={handleExportCSV}>Export CSV</button>
      <button onClick={handleExportJSON}>Export JSON</button>
      <button onClick={handleExportFromAPI}>Export from API</button>
      <button onClick={handleCopy}>Copy to Clipboard</button>
    </div>
  );
}

Complete Example

See ArticleListExample.tsx for a complete implementation using all utilities.

Key Features Demonstrated:

  • Paginated data fetching with search and filters
  • Batch selection
  • Toast notifications
  • Data table with sorting
  • Export to CSV
  • Delete operations
  • Loading and error states

Benefits

Before (Without Utilities)

// 100+ lines of boilerplate for a list page
function ArticleList() {
  const [articles, setArticles] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState([]);
  const [search, setSearch] = useState('');
  
  useEffect(() => {
    setLoading(true);
    fetch(`/api/articles?page=${page}&search=${search}`)
      .then(res => res.json())
      .then(data => {
        setArticles(data.data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [page, search]);

  // More boilerplate...
}

After (With Utilities)

// 30 lines with full functionality
function ArticleList() {
  const { data, meta, loading, error, setSearch, setPage } = 
    usePaginatedData('/articles');
  const selection = useBatchSelection(data, 'id');
  const toast = useToast();

  return (
    <div>
      <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
      <DataTable
        data={data}
        columns={columns}
        loading={loading}
        selectable
        {...selection}
      />
    </div>
  );
}

Result: 70% less code, more features, better UX!


Files Created

Hooks:

  1. hooks/usePaginatedData.ts - Paginated data fetching
  2. hooks/useApiMutation.ts - API mutations (POST, PUT, DELETE)
  3. hooks/useFormValidation.ts - Form validation
  4. hooks/useQueryBuilder.ts - Query string builder
  5. hooks/useToast.ts - Toast notifications
  6. hooks/useBatchSelection.ts - Batch selection

Components: 7. components/common/DataTable.tsx - Data table component 8. components/common/DataTable.css - Table styles 9. components/common/ToastContainer.tsx - Toast container 10. components/common/ToastContainer.css - Toast styles

Utilities: 11. utils/export.ts - Export functions

Examples: 12. components/examples/ArticleListExample.tsx - Complete example 13. components/examples/ArticleListExample.css - Example styles

Documentation: 14. FRONTEND_UTILITIES_GUIDE.md - This guide


Quick Reference

// Fetch paginated data
const { data, meta, loading, setSearch, setPage } = 
  usePaginatedData<T>('/endpoint');

// API mutations
const { mutate, loading, error } = useApiPost<T>('/endpoint');

// Form validation
const { values, errors, handleChange, handleSubmit } = 
  useFormValidation(initialValues, rules);

// Query builder
const { queryString, setFilter, setSearch, setSort } = useQueryBuilder();

// Toast notifications
const toast = useToast();
toast.success('Success!');
toast.error('Error!');

// Batch selection
const selection = useBatchSelection(items, 'id');
selection.toggle(id);
selection.selectAll();

// Export
exportToCSV(data, 'export.csv');
exportToJSON(data, 'export.json');

Integration with Backend

All frontend utilities are designed to work seamlessly with the backend helpers:

Frontend Backend Purpose
usePaginatedData Paginator Pagination
useQueryBuilder QueryParser Filtering/Sorting
useFormValidation Validator Validation
useToast Respond User Feedback
useBatchSelection BatchOps Batch Operations
exportToCSV Exporter Data Export

Best Practices

  1. Always use error boundaries with data fetching hooks
  2. Debounce search inputs to avoid excessive API calls
  3. Show loading states for better UX
  4. Use toast notifications for user feedback
  5. Implement batch operations for efficiency
  6. Export functionality for reporting needs
  7. TypeScript types for type safety

Next Steps

  1. Review the complete example: ArticleListExample.tsx
  2. Copy patterns for your own components
  3. Customize styles to match your design
  4. Add more custom validators as needed
  5. Extend utilities with project-specific features

Your frontend development is now easier, faster, and better! 🚀