You're staring at 10,000 customer records. Somewhere in there is "Sarah Martinez from Acme Corp who signed up in Q3 2024." Without filtering, you're scrolling forever or hoping pagination luck finds her on page 247. With filtering, you type "Sarah" in the name column, select "Acme" in the company filter, and boom—one row. Two seconds.
Filtering is the difference between usable data grids and frustrating ones. It transforms tables from static displays into powerful search and exploration tools. But implementation varies wildly depending on data size, backend capabilities, and user requirements.
In this guide, we'll cover both client-side filtering (fast, simple, works offline) and server-side filtering (scales to millions of rows). You'll learn when to use each, how to implement them in React, and best practices for filter UX.
Client-Side vs Server-Side: Which to Choose?
Client-Side Filtering
Filter data in the browser using JavaScript. All data is loaded upfront, filtering happens instantly.
Pros
- • Instant feedback: No network latency
- • Simple to implement: Just filter an array
- • Works offline: No server required
- • Multi-column filtering: Easy to combine filters
Cons
- • Limited scale: Only works for ~10K rows max
- • Initial load time: Must fetch all data upfront
- • Memory usage: Stores full dataset in browser
Server-Side Filtering
Filter data on the backend. Send filter criteria to server, receive filtered results.
Pros
- • Scales infinitely: Works with millions of rows
- • Fast initial load: Only fetch what's needed
- • Low memory: Browser doesn't store full dataset
- • Complex queries: Database-level filtering
Cons
- • Network latency: Each filter triggers API call
- • Backend work: Requires server-side implementation
- • Requires internet: Doesn't work offline
Rule of thumb: Use client-side filtering for <10K rows. Use server-side for larger datasets or when initial load time matters. Many apps use client-side for simplicity.
Client-Side Filtering with Simple Table
Simple Table has built-in client-side filtering. Just mark columns as filterable, and the table adds filter inputs automatically.
Basic Example: Customer Table
1import { SimpleTable, HeaderObject } from "simple-table-core";2import "simple-table-core/styles.css";34const headers: HeaderObject[] = [5 {6 accessor: "id",7 label: "ID",8 width: 80,9 isSortable: true,10 type: "number",11 filterable: true, // Enable filtering with 10 operators12 },13 {14 accessor: "name",15 label: "Customer Name",16 width: 200,17 isSortable: true,18 filterable: true, // Enable filtering with 8 string operators19 type: "string",20 },21 {22 accessor: "company",23 label: "Company",24 width: 180,25 isSortable: true,26 filterable: true,27 type: "string",28 },29 {30 accessor: "status",31 label: "Status",32 width: 120,33 isSortable: true,34 filterable: true, // Enable enum filtering with 4 operators35 type: "enum",36 enumOptions: [37 { label: "Active", value: "active" },38 { label: "Inactive", value: "inactive" },39 { label: "Pending", value: "pending" },40 ],41 },42 {43 accessor: "revenue",44 label: "Revenue",45 width: 120,46 isSortable: true,47 filterable: true, // Number filtering with 10 operators48 type: "number",49 align: "right",50 valueFormatter: ({ value }) =>51 new Intl.NumberFormat('en-US', {52 style: 'currency',53 currency: 'USD'54 }).format(value as number),55 },56 {57 accessor: "signupDate",58 label: "Signup Date",59 width: 140,60 isSortable: true,61 filterable: true, // Date filtering with 8 operators62 type: "date",63 },64 {65 accessor: "isPremium",66 label: "Premium",67 width: 100,68 isSortable: true,69 filterable: true, // Boolean filtering with 3 operators70 type: "boolean",71 },72];7374export default function CustomerTable({ data }) {75 return (76 <SimpleTable77 defaultHeaders={headers}78 rows={data}79 rowIdAccessor="id"80 height="600px"81 theme="light"82 />83 );84}
That's it! Users can now click the filter icon in any header to filter that column. Simple Table provides intelligent filtering with different operators for each data type—automatically.
Intelligent Type-Based Filtering
Simple Table automatically provides appropriate filter operators based on column type. This works seamlessly with column sorting and pagination:
📝 String Filtering (8 operators)
Contains, Does not contain, Equals, Does not equal, Starts with, Ends with, Is empty, Is not empty
Case-insensitive by default. Perfect for names, emails, descriptions.
🔢 Number Filtering (10 operators)
Equals, Does not equal, Greater than, Greater than or equal, Less than, Less than or equal, Between, Not between, Is empty, Is not empty
Perfect for prices, quantities, ages, scores.
📅 Date Filtering (8 operators)
Equals, Does not equal, Before, After, Between, Not between, Is empty, Is not empty
Date picker UI for easy date range selection.
✓ Boolean Filtering (3 operators)
Is true, Is false, Is empty
Simple true/false/null filtering for flags and toggles.
📋 Enum Filtering (4 operators + search)
Is one of, Is not one of, Is empty, Is not empty
Multi-select from predefined options. Auto-adds search input when more than 10 options.
Server-Side Filtering Implementation
For large datasets, you need to send filter criteria to your backend and fetch filtered results. Here's how to implement it:
Using onFilterChange for Server-Side Filtering
Simple Table provides built-in filter UI with intelligent operators. For server-side filtering, use `onFilterChange` to receive filter state and fetch filtered data from your API:
1import { SimpleTable, HeaderObject, TableFilterState } from "simple-table-core";2import { useState, useEffect } from "react";34export default function ServerSideFilterTable() {5 const [data, setData] = useState([]);6 const [filters, setFilters] = useState<TableFilterState>({});7 const [loading, setLoading] = useState(false);89 const headers: HeaderObject[] = [10 {11 accessor: "name",12 label: "Customer Name",13 width: 200,14 filterable: true, // Keep filter UI15 type: "string",16 },17 {18 accessor: "status",19 label: "Status",20 width: 120,21 filterable: true,22 type: "enum",23 enumOptions: [24 { label: "Active", value: "active" },25 { label: "Inactive", value: "inactive" },26 { label: "Pending", value: "pending" },27 ],28 },29 {30 accessor: "revenue",31 label: "Revenue",32 width: 120,33 filterable: true,34 type: "number",35 },36 // ... other columns37 ];3839 // Fetch data whenever filters change40 useEffect(() => {41 const fetchFilteredData = async () => {42 setLoading(true);4344 // Convert filter state to API params45 const params = new URLSearchParams();46 Object.entries(filters).forEach(([filterId, filter]) => {47 params.append(filterId, JSON.stringify(filter));48 });4950 const response = await fetch(`/api/customers?${params}`);51 const result = await response.json();5253 setData(result);54 setLoading(false);55 };5657 // Debounce API calls58 const timeoutId = setTimeout(fetchFilteredData, 300);59 return () => clearTimeout(timeoutId);60 }, [filters]);6162 return (63 <div>64 {loading && <div className="mb-2">Loading filtered results...</div>}6566 <SimpleTable67 defaultHeaders={headers}68 rows={data}69 rowIdAccessor="id"70 height="600px"71 onFilterChange={(newFilters) => setFilters(newFilters)}72 externalFilterHandling={true} // Disable client-side filtering73 />74 </div>75 );76}
Key points: Set `externalFilterHandling=` to disable internal filtering. Simple Table still shows the filter UI with all operators, but you handle the actual filtering via API. Users get the same great filter experience while you control the backend logic.
Backend Implementation (Node.js Example)
1// API route: /api/customers2app.get('/api/customers', async (req, res) => {3 const { page = 1, limit = 50 } = req.query;4 let query = db.select('*').from('customers');56 // Parse filter parameters7 Object.entries(req.query).forEach(([key, value]) => {8 if (key.startsWith('filter_')) {9 const filter = JSON.parse(value);10 const columnName = filter.column;11 const operator = filter.operator;12 const filterValue = filter.value;1314 // Apply filter based on operator15 switch (operator) {16 case 'contains':17 query = query.where(columnName, 'like', `%${filterValue}%`);18 break;19 case 'equals':20 query = query.where(columnName, '=', filterValue);21 break;22 case 'greaterThan':23 query = query.where(columnName, '>', filterValue);24 break;25 case 'lessThan':26 query = query.where(columnName, '<', filterValue);27 break;28 case 'between':29 query = query.whereBetween(columnName, [filterValue.min, filterValue.max]);30 break;31 case 'isOneOf':32 query = query.whereIn(columnName, filterValue);33 break;34 // ... handle other operators35 }36 }37 });3839 // Add pagination40 const offset = (page - 1) * limit;41 query = query.limit(limit).offset(offset);4243 const results = await query;44 res.json(results);45});
Filter UX Best Practices
🎯 Show Active Filters Clearly
Users should always know which filters are active. Use badges, highlights, or a "Active Filters" summary above the table.
⚡ Debounce Text Input
Wait 300-500ms after user stops typing before applying filters. Prevents excessive API calls or re-renders while typing.
💾 Persist Filter State
Save filter criteria in URL query params or localStorage. Users expect filters to persist when navigating away and returning.
🧹 "Clear All" Button
Always provide a one-click way to clear all filters. Users get lost in filtered views—give them an escape hatch.
📊 Show Result Counts
Display "Showing 47 of 10,000 results" so users understand the impact of their filters. Empty results should explain why.
🚀 Loading States
For server-side filtering, show a loading indicator while fetching. Skeleton rows or a spinner prevent confusion.
Advanced Filtering Patterns
Saved Filter Presets
Let users save complex filter combinations for quick reuse:
1const [savedFilters, setSavedFilters] = useState([2 { name: "High Value Active", filters: { status: ["Active"], revenue: ">100000" } },3 { name: "Q4 Signups", filters: { signupDate: "2024-10-01,2024-12-31" } },4]);56// UI to apply saved filters7<select onChange={(e) => applyFilterPreset(e.target.value)}>8 <option>Select a preset...</option>9 {savedFilters.map(preset => (10 <option key={preset.name} value={preset.name}>11 {preset.name}12 </option>13 ))}14</select>
Smart Filter Suggestions
As users type, suggest common values from the dataset:
1const [suggestions, setSuggestions] = useState([]);23const handleInputChange = (value: string) => {4 // Fetch suggestions from backend5 fetch(`/api/suggestions?column=name&query=${value}`)6 .then(res => res.json())7 .then(data => setSuggestions(data));8};910// Render autocomplete dropdown11<input12 type="text"13 onChange={(e) => handleInputChange(e.target.value)}14 list="suggestions"15/>16<datalist id="suggestions">17 {suggestions.map(item => (18 <option key={item.value} value={item.value} />19 ))}20</datalist>
Hybrid Approach: Client + Server
Fetch first 10K rows, filter client-side. If user needs more, fetch from server:
1const [mode, setMode] = useState<'client' | 'server'>('client');2const [allData, setAllData] = useState([]);34useEffect(() => {5 // Initially fetch first 10K rows6 fetch('/api/customers?limit=10000')7 .then(res => res.json())8 .then(data => {9 setAllData(data);10 setMode('client');11 });12}, []);1314const handleFilterChange = (filters) => {15 if (mode === 'client') {16 // Try client-side filtering first17 const filtered = allData.filter(row => matchesFilters(row, filters));1819 if (filtered.length < 50 && allData.length === 10000) {20 // Might be more results on server21 setMode('server');22 fetchFromServer(filters);23 }24 } else {25 fetchFromServer(filters);26 }27};
Filtering: Essential for Data Exploration
Without filtering, data grids are just static displays. With filtering, they become powerful exploration tools. Whether you're building CRMs, admin panels, or analytics dashboards, filtering transforms how users interact with data.
- Use client-side filtering for datasets under 10K rows
- Use server-side filtering for large datasets or fast initial load
- Debounce text inputs to avoid excessive API calls
- Show active filters clearly and provide a "Clear All" button
- Persist filter state in URL params for shareable filtered views
Simple Table makes client-side filtering trivial with built-in support. For server-side, you have full control to implement custom filter UI and integrate with your backend. Choose the approach that fits your data size and requirements.