React Grid Filtering: Client-Side vs Server-Side Implementation

FilteringTutorialSearch

Users need to find specific data fast. Learn how to implement powerful filtering in React data grids—from simple text search to advanced multi-column filters, with both client-side and server-side approaches.

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

React TSX
1import { SimpleTable, HeaderObject } from "simple-table-core";
2import "simple-table-core/styles.css";
3
4const 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 operators
12 },
13 {
14 accessor: "name",
15 label: "Customer Name",
16 width: 200,
17 isSortable: true,
18 filterable: true, // Enable filtering with 8 string operators
19 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 operators
35 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 operators
48 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 operators
62 type: "date",
63 },
64 {
65 accessor: "isPremium",
66 label: "Premium",
67 width: 100,
68 isSortable: true,
69 filterable: true, // Boolean filtering with 3 operators
70 type: "boolean",
71 },
72];
73
74export default function CustomerTable({ data }) {
75 return (
76 <SimpleTable
77 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:

React TSX
1import { SimpleTable, HeaderObject, TableFilterState } from "simple-table-core";
2import { useState, useEffect } from "react";
3
4export default function ServerSideFilterTable() {
5 const [data, setData] = useState([]);
6 const [filters, setFilters] = useState<TableFilterState>({});
7 const [loading, setLoading] = useState(false);
8
9 const headers: HeaderObject[] = [
10 {
11 accessor: "name",
12 label: "Customer Name",
13 width: 200,
14 filterable: true, // Keep filter UI
15 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 columns
37 ];
38
39 // Fetch data whenever filters change
40 useEffect(() => {
41 const fetchFilteredData = async () => {
42 setLoading(true);
43
44 // Convert filter state to API params
45 const params = new URLSearchParams();
46 Object.entries(filters).forEach(([filterId, filter]) => {
47 params.append(filterId, JSON.stringify(filter));
48 });
49
50 const response = await fetch(`/api/customers?${params}`);
51 const result = await response.json();
52
53 setData(result);
54 setLoading(false);
55 };
56
57 // Debounce API calls
58 const timeoutId = setTimeout(fetchFilteredData, 300);
59 return () => clearTimeout(timeoutId);
60 }, [filters]);
61
62 return (
63 <div>
64 {loading && <div className="mb-2">Loading filtered results...</div>}
65
66 <SimpleTable
67 defaultHeaders={headers}
68 rows={data}
69 rowIdAccessor="id"
70 height="600px"
71 onFilterChange={(newFilters) => setFilters(newFilters)}
72 externalFilterHandling={true} // Disable client-side filtering
73 />
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)

React TSX
1// API route: /api/customers
2app.get('/api/customers', async (req, res) => {
3 const { page = 1, limit = 50 } = req.query;
4 let query = db.select('*').from('customers');
5
6 // Parse filter parameters
7 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;
13
14 // Apply filter based on operator
15 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 operators
35 }
36 }
37 });
38
39 // Add pagination
40 const offset = (page - 1) * limit;
41 query = query.limit(limit).offset(offset);
42
43 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:

React TSX
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]);
5
6// UI to apply saved filters
7<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:

React TSX
1const [suggestions, setSuggestions] = useState([]);
2
3const handleInputChange = (value: string) => {
4 // Fetch suggestions from backend
5 fetch(`/api/suggestions?column=name&query=${value}`)
6 .then(res => res.json())
7 .then(data => setSuggestions(data));
8};
9
10// Render autocomplete dropdown
11<input
12 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:

React TSX
1const [mode, setMode] = useState<'client' | 'server'>('client');
2const [allData, setAllData] = useState([]);
3
4useEffect(() => {
5 // Initially fetch first 10K rows
6 fetch('/api/customers?limit=10000')
7 .then(res => res.json())
8 .then(data => {
9 setAllData(data);
10 setMode('client');
11 });
12}, []);
13
14const handleFilterChange = (filters) => {
15 if (mode === 'client') {
16 // Try client-side filtering first
17 const filtered = allData.filter(row => matchesFilters(row, filters));
18
19 if (filtered.length < 50 && allData.length === 10000) {
20 // Might be more results on server
21 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.

Ready to add powerful filtering to your React tables?

Simple Table provides built-in client-side filtering for instant search, or gives you the flexibility to implement custom server-side filtering for massive datasets. Start building filterable tables in minutes.