Client Sorting
Client Sorting
Sort already-loaded data on the client.
Basic Sort Hook
// hooks/useSorting.js
function useSorting(items, defaultSort = { key: null, direction: 'asc' }) {
const [sortConfig, setSortConfig] = useState(defaultSort);
const sortedItems = useMemo(() => {
if (!sortConfig.key) return items;
return [...items].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal === bVal) return 0;
if (aVal === null || aVal === undefined) return 1;
if (bVal === null || bVal === undefined) return -1;
let comparison = 0;
if (typeof aVal === 'string') {
comparison = aVal.localeCompare(bVal);
} else if (typeof aVal === 'number') {
comparison = aVal - bVal;
} else if (aVal instanceof Date) {
comparison = aVal.getTime() - bVal.getTime();
} else if (typeof aVal === 'boolean') {
comparison = aVal === bVal ? 0 : aVal ? -1 : 1;
}
return sortConfig.direction === 'desc' ? -comparison : comparison;
});
}, [items, sortConfig]);
const requestSort = useCallback((key) => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
}));
}, []);
const clearSort = useCallback(() => {
setSortConfig({ key: null, direction: 'asc' });
}, []);
return { sortedItems, sortConfig, requestSort, clearSort };
}
Sort Button Component
// components/SortButton.jsx
function SortButton({ label, sortKey, currentSort, onSort }) {
const isActive = currentSort.key === sortKey;
const direction = isActive ? currentSort.direction : null;
return (
<button
className={`sort-button ${isActive ? 'active' : ''}`}
onClick={() => onSort(sortKey)}
aria-label={`Sort by ${label} ${direction === 'asc' ? 'ascending' : 'descending'}`}
>
{label}
{isActive && (
<span className="sort-icon">
{direction === 'asc' ? '↑' : '↓'}
</span>
)}
</button>
);
}
// Usage
function ProductTable({ products }) {
const { sortedItems, sortConfig, requestSort } = useSorting(products);
return (
<table>
<thead>
<tr>
<th>
<SortButton
label="Name"
sortKey="name"
currentSort={sortConfig}
onSort={requestSort}
/>
</th>
<th>
<SortButton
label="Price"
sortKey="price"
currentSort={sortConfig}
onSort={requestSort}
/>
</th>
<th>
<SortButton
label="Rating"
sortKey="rating"
currentSort={sortConfig}
onSort={requestSort}
/>
</th>
</tr>
</thead>
<tbody>
{sortedItems.map(product => (
<tr key={product.id}>
<td>{product.name}</td>
<td>${product.price.toFixed(2)}</td>
<td>{product.rating}★</td>
</tr>
))}
</tbody>
</table>
);
}
Sort State in URL
function useURLSort(defaultSort = 'relevance') {
const [searchParams, setSearchParams] = useSearchParams();
const sortBy = searchParams.get('sort') || defaultSort;
const sortDir = searchParams.get('dir') || 'asc';
const setSort = useCallback((key, direction = 'asc') => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.set('sort', key);
params.set('dir', direction);
return params;
});
}, [setSearchParams]);
return { sortBy, sortDir, setSort };
}
Server Sorting
Server Sorting
Delegate sorting to the server for large datasets.
Server Sort Hook
// hooks/useServerSort.js
function useServerSort({ endpoint, defaultSort = 'createdAt', defaultDir = 'desc' }) {
const [sortConfig, setSortConfig] = useState({
key: defaultSort,
direction: defaultDir,
});
const { data, isLoading, error } = useQuery({
queryKey: ['sorted', endpoint, sortConfig],
queryFn: async () => {
const params = new URLSearchParams({
sortBy: sortConfig.key,
sortDir: sortConfig.direction,
});
const response = await fetch(`${endpoint}?${params}`);
return response.json();
},
});
const requestSort = useCallback((key) => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
}));
}, []);
return {
items: data?.items || [],
total: data?.total || 0,
sortConfig,
requestSort,
isLoading,
error,
};
}
Sort Dropdown
function SortDropdown({ value, onChange }) {
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
<option value="relevance">Relevance</option>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="price-asc">Price (Low to High)</option>
<option value="price-desc">Price (High to Low)</option>
<option value="rating-desc">Rating (High to Low)</option>
<option value="newest">Newest First</option>
</select>
);
}
// Usage
function ProductList() {
const { items, sortConfig, requestSort, isLoading } = useServerSort({
endpoint: '/api/products',
});
return (
<div>
<div className="sort-controls">
<SortDropdown
value={`${sortConfig.key}-${sortConfig.direction}`}
onChange={(value) => {
const [key, dir] = value.split('-');
requestSort(key);
}}
/>
</div>
{isLoading ? <Spinner /> : <ProductGrid products={items} />}
</div>
);
}
API Response Pattern
// Server response format
// GET /api/products?sortBy=price&sortDir=asc&page=1&limit=20
// Response
{
"items": [...],
"total": 100,
"page": 1,
"limit": 20,
"sortBy": "price",
"sortDir": "asc"
}
Multi-Column Sort
Multi-Column Sort
Allow sorting by multiple columns simultaneously.
Multi-Column Sort Hook
// hooks/useMultiSort.js
function useMultiSort(items, defaultSort = []) {
const [sorts, setSorts] = useState(defaultSort);
const sortedItems = useMemo(() => {
if (sorts.length === 0) return items;
return [...items].sort((a, b) => {
for (const { key, direction } of sorts) {
const aVal = a[key];
const bVal = b[key];
if (aVal === bVal) continue;
if (aVal === null || aVal === undefined) return 1;
if (bVal === null || bVal === undefined) return -1;
let comparison = 0;
if (typeof aVal === 'string') {
comparison = aVal.localeCompare(bVal);
} else if (typeof aVal === 'number') {
comparison = aVal - bVal;
}
return direction === 'desc' ? -comparison : comparison;
}
return 0;
});
}, [items, sorts]);
const addSort = useCallback((key, direction = 'asc') => {
setSorts(prev => {
const existing = prev.findIndex(s => s.key === key);
if (existing >= 0) {
if (prev[existing].direction === direction) {
return prev.filter((_, i) => i !== existing);
}
return prev.map((s, i) =>
i === existing ? { ...s, direction } : s
);
}
return [...prev, { key, direction }];
});
}, []);
const clearSort = useCallback(() => {
setSorts([]);
}, []);
const removeSort = useCallback((key) => {
setSorts(prev => prev.filter(s => s.key !== key));
}, []);
return { sortedItems, sorts, addSort, removeSort, clearSort };
}
Sort Priority UI
function SortPriorityList({ sorts, onRemove, onReorder }) {
if (sorts.length === 0) return null;
return (
<div className="sort-priority">
<h4>Sort Priority</h4>
<ul>
{sorts.map((sort, index) => (
<li key={sort.key} className="sort-item">
<span className="sort-number">{index + 1}</span>
<span>{sort.key}</span>
<span className="sort-direction">
{sort.direction === 'asc' ? '↑' : '↓'}
</span>
<button onClick={() => onRemove(sort.key)}>×</button>
{index > 0 && (
<button onClick={() => onReorder(index, index - 1)}>↑</button>
)}
{index < sorts.length - 1 && (
<button onClick={() => onReorder(index, index + 1)}>↓</button>
)}
</li>
))}
</ul>
</div>
);
}
Advanced Sort Table
function SortableTable({ data, columns }) {
const { sortedItems, sorts, addSort, removeSort, clearSort } = useMultiSort(data);
return (
<div>
<SortPriorityList sorts={sorts} onRemove={removeSort} />
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.key}>
<button
onClick={() => addSort(col.key)}
className="sort-header"
>
{col.label}
{sorts.find(s => s.key === col.key) && (
<span>
{sorts.find(s => s.key === col.key).direction === 'asc' ? ' ↑' : ' ↓'}
</span>
)}
</button>
</th>
))}
</tr>
</thead>
<tbody>
{sortedItems.map((row, i) => (
<tr key={row.id || i}>
{columns.map(col => (
<td key={col.key}>{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
Practice Problems
Create a reusable React component implementing Sorting. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Sorting using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Sorting for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. How do you toggle sort direction when clicking the same column?
2. When should you use server-side sorting?
3. How does multi-column sort determine priority?
4. Why show sort indicators to users?
Flashcards
Question
How do you implement ascending/descending toggle?
Click to reveal answer
Answer
Check if clicking the same column and flip direction; otherwise, set ascending for new column.
Question
When should you use server-side sorting?
Click to reveal answer
Answer
For paginated data or datasets too large to load entirely on the client.
Question
What is multi-column sort?
Click to reveal answer
Answer
Sorting by multiple criteria where each subsequent sort only applies to items with equal values in previous sorts.
Question
Why memoize sort calculations?
Click to reveal answer
Answer
To avoid recalculating the entire sort on every render when items or sort config haven't changed.
Question
What is Sorting?
Click to reveal answer
Answer
Sorting is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Client-side sorting works for small, loaded datasets
- 2.Server-side sorting is needed for paginated data
- 3.Toggle direction when clicking the same sort column
- 4.Multi-column sort uses priority order for tiebreaking
- 5.Always show sort indicators to users
Interview Tips
- •Explain how to implement ascending/descending toggle
- •Discuss when to use client vs server sorting
- •Know how multi-column sort priority works
Cheat Sheet
Sorting Cheat Sheet
Client Sort
const sorted = [...items].sort((a, b) => {
if (a[key] < b[key]) return direction === 'asc' ? -1 : 1;
if (a[key] > b[key]) return direction === 'asc' ? 1 : -1;
return 0;
});
Server Sort
fetch(`/api/items?sortBy=${key}&sortDir=${direction}`);
Toggle Direction
const newDirection = currentKey === key && currentDir === 'asc' ? 'desc' : 'asc';