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
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:
- ✅
hooks/usePaginatedData.ts- Paginated data fetching - ✅
hooks/useApiMutation.ts- API mutations (POST, PUT, DELETE) - ✅
hooks/useFormValidation.ts- Form validation - ✅
hooks/useQueryBuilder.ts- Query string builder - ✅
hooks/useToast.ts- Toast notifications - ✅
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
- Always use error boundaries with data fetching hooks
- Debounce search inputs to avoid excessive API calls
- Show loading states for better UX
- Use toast notifications for user feedback
- Implement batch operations for efficiency
- Export functionality for reporting needs
- TypeScript types for type safety
Next Steps
- Review the complete example:
ArticleListExample.tsx - Copy patterns for your own components
- Customize styles to match your design
- Add more custom validators as needed
- Extend utilities with project-specific features
Your frontend development is now easier, faster, and better! 🚀