Client Filtering
Client Filtering
Filter already-loaded data on the client side.
Basic Filter Hook
// hooks/useFilters.js
function useFilters(items, filterConfig) {
const [filters, setFilters] = useState({});
const filteredItems = useMemo(() => {
return items.filter(item => {
return Object.entries(filters).every(([key, value]) => {
if (!value || (Array.isArray(value) && value.length === 0)) return true;
const filterFn = filterConfig[key];
if (filterFn) return filterFn(item[key], value);
if (Array.isArray(value)) return value.includes(item[key]);
return item[key] === value;
});
});
}, [items, filters, filterConfig]);
const updateFilter = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
}, []);
const resetFilters = useCallback(() => {
setFilters({});
}, []);
const activeFilterCount = Object.values(filters).filter(v =>
v && (!Array.isArray(v) || v.length > 0)
).length;
return { filters, filteredItems, updateFilter, resetFilters, activeFilterCount };
}
Filter Components
// components/FilterPanel.jsx
function FilterPanel({ filters, onChange, config }) {
return (
<div className="filter-panel">
{config.map(filter => (
<FilterItem
key={filter.key}
filter={filter}
value={filters[filter.key]}
onChange={(value) => onChange(filter.key, value)}
/>
))}
</div>
);
}
function FilterItem({ filter, value, onChange }) {
switch (filter.type) {
case 'select':
return (
<select value={value || ''} onChange={(e) => onChange(e.target.value)}>
<option value="">All {filter.label}</option>
{filter.options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
);
case 'checkbox':
return (
<label className="checkbox-filter">
<input
type="checkbox"
checked={value || false}
onChange={(e) => onChange(e.target.checked)}
/>
{filter.label}
</label>
);
case 'range':
return (
<RangeSlider
min={filter.min}
max={filter.max}
value={value || [filter.min, filter.max]}
onChange={onChange}
/>
);
default:
return null;
}
}
Usage Example
function ProductCatalog({ products }) {
const filterConfig = [
{
key: 'category',
label: 'Category',
type: 'select',
options: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
{ value: 'home', label: 'Home & Garden' },
],
},
{
key: 'inStock',
label: 'In Stock Only',
type: 'checkbox',
},
{
key: 'price',
label: 'Price Range',
type: 'range',
min: 0,
max: 1000,
},
];
const { filters, filteredItems, updateFilter, resetFilters, activeFilterCount } =
useFilters(products, filterConfig);
return (
<div className="catalog">
<FilterPanel
filters={filters}
onChange={updateFilter}
config={filterConfig}
/>
<div className="results">
<div className="results-header">
<span>{filteredItems.length} products</span>
{activeFilterCount > 0 && (
<button onClick={resetFilters}>Clear filters</button>
)}
</div>
<ProductGrid products={filteredItems} />
</div>
</div>
);
}
Server Filtering
Server Filtering
Send filter parameters to the server for large datasets.
Filter API Integration
// hooks/useServerFilters.js
function useServerFilters({ endpoint, initialFilters = {} }) {
const [filters, setFilters] = useState(initialFilters);
const debouncedFilters = useDebounce(filters, 300);
const { data, isLoading, error } = useQuery({
queryKey: ['filtered', endpoint, debouncedFilters],
queryFn: async () => {
const params = new URLSearchParams();
Object.entries(debouncedFilters).forEach(([key, value]) => {
if (value && (!Array.isArray(value) || value.length > 0)) {
params.set(key, JSON.stringify(value));
}
});
const response = await fetch(`${endpoint}?${params}`);
return response.json();
},
});
return {
filters,
setFilters,
items: data?.items || [],
total: data?.total || 0,
isLoading,
error,
};
}
Filter Presets
const filterPresets = {
all: {},
onSale: { onSale: true },
inStock: { inStock: true, minPrice: 0, maxPrice: 100 },
premium: { minPrice: 100, rating: 4.5 },
};
function FilterPresets({ onSelect }) {
return (
<div className="filter-presets">
{Object.entries(filterPresets).map(([name, filters]) => (
<button key={name} onClick={() => onSelect(filters)}>
{name.replace(/([A-Z])/g, ' $1').trim()}
</button>
))}
</div>
);
}
Active Filters Display
function ActiveFilters({ filters, onRemove, onClearAll }) {
const activeFilters = Object.entries(filters).filter(([_, value]) =>
value && (!Array.isArray(value) || value.length > 0)
);
if (activeFilters.length === 0) return null;
return (
<div className="active-filters">
{activeFilters.map(([key, value]) => (
<span key={key} className="filter-tag">
{key}: {formatFilterValue(value)}
<button onClick={() => onRemove(key)}>×</button>
</span>
))}
<button onClick={onClearAll} className="clear-all">
Clear all
</button>
</div>
);
}
Performance Considerations
// Memoize filter functions
const filterFns = useMemo(() => ({
category: (item, value) => item.category === value,
price: (item, [min, max]) => item.price >= min && item.price <= max,
rating: (item, minRating) => item.rating >= minRating,
}), []);
// Use indexed data for large datasets
const categoryIndex = useMemo(() => {
const index = {};
items.forEach(item => {
if (!index[item.category]) index[item.category] = [];
index[item.category].push(item);
});
return index;
}, [items]);
URL State Sync
URL State Sync
Keep filter state in the URL for shareability and bookmarking.
URL State Hook
// hooks/useURLFilters.js
function useURLFilters(defaultFilters = {}) {
const [searchParams, setSearchParams] = useSearchParams();
const filters = useMemo(() => {
const params = Object.fromEntries(searchParams);
const parsed = {};
Object.entries(defaultFilters).forEach(([key, defaultValue]) => {
const value = params[key];
if (value === undefined) {
parsed[key] = defaultValue;
} else if (typeof defaultValue === 'boolean') {
parsed[key] = value === 'true';
} else if (Array.isArray(defaultValue)) {
parsed[key] = value.split(',');
} else {
parsed[key] = value;
}
});
return parsed;
}, [searchParams, defaultFilters]);
const setFilters = useCallback((newFilters) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
Object.entries(newFilters).forEach(([key, value]) => {
if (value === undefined || value === null || value === '' ||
(Array.isArray(value) && value.length === 0)) {
params.delete(key);
} else if (typeof value === 'boolean') {
params.set(key, value.toString());
} else if (Array.isArray(value)) {
params.set(key, value.join(','));
} else {
params.set(key, value);
}
});
return params;
});
}, [setSearchParams]);
return { filters, setFilters };
}
Filter Component with URL Sync
function ProductFilters() {
const { filters, setFilters } = useURLFilters({
category: '',
minPrice: 0,
maxPrice: 1000,
inStock: false,
sort: 'relevance',
});
return (
<div className="filters">
<select
value={filters.category}
onChange={(e) => setFilters({ category: e.target.value })}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => setFilters({ inStock: e.target.checked })}
/>
In Stock Only
</label>
<PriceRange
min={filters.minPrice}
max={filters.maxPrice}
onChange={(min, max) => setFilters({ minPrice: min, maxPrice: max })}
/>
</div>
);
}
// Results component reads from URL
function ProductResults() {
const { filters } = useURLFilters({});
const { data, isLoading } = useQuery({
queryKey: ['products', filters],
queryFn: () => fetchProducts(filters),
});
return <ProductGrid products={data?.items || []} isLoading={isLoading} />;
}
Benefits of URL Sync
- Shareable filter states
- Browser back/forward works
- Bookmarkable searches
- Analytics tracking
- Refresh preserves state
Practice Problems
Create a reusable React component implementing Filtering. 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 Filtering using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Filtering 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. When should you use client-side filtering?
2. Why sync filters with URL?
3. How should you handle filter changes in server-side filtering?
4. What is a filter preset?
Flashcards
Question
What is the difference between client and server filtering?
Click to reveal answer
Answer
Client filtering runs in the browser on loaded data; server filtering sends parameters to the API.
Question
Why debounce filter API calls?
Click to reveal answer
Answer
To avoid overwhelming the server with requests while the user is still adjusting filters.
Question
How do you sync filters with URL?
Click to reveal answer
Answer
Store filter values as URL search params and read them back on component mount.
Question
What are filter presets?
Click to reveal answer
Answer
Predefined filter configurations that users can apply with one click.
Question
What is Filtering?
Click to reveal answer
Answer
Filtering is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Client-side filtering is best for small, loaded datasets
- 2.Debounce server filter requests to reduce API calls
- 3.Sync filters with URL for shareability and navigation
- 4.Provide filter presets for common configurations
- 5.Show active filters and allow easy removal
Interview Tips
- •Explain when to use client vs server filtering
- •Describe how to implement URL-based filter state
- •Know how to optimize filter performance
Cheat Sheet
Filtering Cheat Sheet
Client Filter
const filtered = items.filter(item =>
item.category === filters.category &&
item.price >= filters.minPrice
);
Server Filter
const params = new URLSearchParams(filters);
fetch(`/api/items?${params}`);
URL Sync
const [searchParams, setSearchParams] = useSearchParams();
const filters = Object.fromEntries(searchParams);