# Filtering Sorting: UI for Data Lists

# Filtering & Sorting: UI for Data Lists with Query String Management

## Problem

Modern web applications need to display large datasets with the ability to:
- Filter data by multiple criteria
- Sort by different columns
- Maintain state across page refreshes
- Share filtered/sorted views via URLs
- Provide intuitive UI controls

Without proper query string management, users lose their filtering/sorting state on refresh, can't share specific views, and experience poor UX.

---

## Solution Architecture

### Core Principles

1. **URL as Source of Truth**: Query strings store all filter/sort state
2. **Bidirectional Sync**: UI ↔ Query String ↔ Data State
3. **Debounced Updates**: Prevent excessive URL rewrites
4. **Type-Safe Parsing**: Validate and sanitize query parameters
5. **Composable Filters**: Build complex queries from simple components

### Key Components

```
┌─────────────────────────────────────────┐
│         Filter/Sort UI Controls         │
│  (Dropdowns, Checkboxes, Search Bars)   │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│    Query String Manager (Parser)        │
│  (Encode/Decode URL Parameters)         │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│      Data Filter & Sort Engine          │
│  (Apply Filters, Sort Results)          │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│         Rendered Data List              │
│  (Table, Cards, Grid)                   │
└─────────────────────────────────────────┘
```

---

## Code Implementation

### 1. Query String Manager

```typescript
// queryManager.ts
interface FilterState {
  search?: string;
  category?: string[];
  priceRange?: [number, number];
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
  page?: number;
}

class QueryStringManager {
  private debounceTimer: NodeJS.Timeout | null = null;
  private debounceDelay = 300;

  // Parse query string to filter state
  parseQueryString(search: string): FilterState {
    const params = new URLSearchParams(search);
    
    return {
      search: params.get('q') || undefined,
      category: params.getAll('cat') || undefined,
      priceRange: this.parsePriceRange(params.get('price')),
      sortBy: params.get('sort') || 'relevance',
      sortOrder: (params.get('order') as 'asc' | 'desc') || 'desc',
      page: parseInt(params.get('page') || '1'),
    };
  }

  // Convert filter state to query string
  encodeQueryString(state: FilterState): string {
    const params = new URLSearchParams();

    if (state.search) params.set('q', state.search);
    if (state.category?.length) {
      state.category.forEach(cat => params.append('cat', cat));
    }
    if (state.priceRange) {
      params.set('price', `${state.priceRange[0]}-${state.priceRange[1]}`);
    }
    if (state.sortBy) params.set('sort', state.sortBy);
    if (state.sortOrder) params.set('order', state.sortOrder);
    if (state.page && state.page > 1) params.set('page', state.page.toString());

    return params.toString();
  }

  private parsePriceRange(priceStr: string | null): [number, number] | undefined {
    if (!priceStr) return undefined;
    const [min, max] = priceStr.split('-').map(Number);
    return [min, max];
  }

  // Update URL without page reload
  updateURL(state: FilterState): void {
    if (this.debounceTimer) clearTimeout(this.debounceTimer);

    this.debounceTimer = setTimeout(() => {
      const queryString = this.encodeQueryString(state);
      const newURL = `${window.location.pathname}?${queryString}`;
      window.history.replaceState({ ...state }, '', newURL);
    }, this.debounceDelay);
  }

  // Get current state from URL
  getCurrentState(): FilterState {
    return this.parseQueryString(window.location.search);
  }
}

export const queryManager = new QueryStringManager();
```

### 2. React Component with Hooks

```typescript
// useFilteredData.ts
import { useState, useEffect, useCallback } from 'react';
import { queryManager } from './queryManager';

interface DataItem {
  id: string;
  name: string;
  category: string;
  price: number;
  rating: number;
}

export function useFilteredData(allData: DataItem[]) {
  const [filterState, setFilterState] = useState<FilterState>(() =>
    queryManager.getCurrentState()
  );
  const [filteredData, setFilteredData] = useState<DataItem[]>(allData);

  // Apply filters and sorting
  const applyFilters = useCallback((state: FilterState, data: DataItem[]) => {
    let result = [...data];

    // Search filter
    if (state.search) {
      const query = state.search.toLowerCase();
      result = result.filter(item =>
        item.name.toLowerCase().includes(query)
      );
    }

    // Category filter
    if (state.category?.length) {
      result = result.filter(item =>
        state.category!.includes(item.category)
      );
    }

    // Price range filter
    if (state.priceRange) {
      const [min, max] = state.priceRange;
      result = result.filter(item =>
        item.price >= min && item.price <= max
      );
    }

    // Sorting
    result.sort((a, b) => {
      let aVal: any = a[state.sortBy as keyof DataItem] || 0;
      let bVal: any = b[state.sortBy as keyof DataItem] || 0;

      const comparison = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
      return state.sortOrder === 'asc' ? comparison : -comparison;
    });

    // Pagination
    const pageSize = 20;
    const startIdx = ((state.page || 1) - 1) * pageSize;
    return result.slice(startIdx, startIdx + pageSize);
  }, []);

  // Update filtered data when state changes
  useEffect(() => {
    setFilteredData(applyFilters(filterState, allData));
  }, [filterState, allData, applyFilters]);

  // Update URL when state changes
  useEffect(() => {
    queryManager.updateURL(filterState);
  }, [filterState]);

  // Handle filter changes
  const updateFilter = useCallback((updates: Partial<FilterState>) => {
    setFilterState(prev => ({
      ...prev,
      ...updates,
      page: 1, // Reset to first page on filter change
    }));
  }, []);

  return { filterState, filteredData, updateFilter };
}
```

### 3. UI Component

```typescript
// DataListWithFilters.tsx
import React from 'react';
import { useFilteredData } from './useFilteredData';

interface Props {
  data: DataItem[];
  categories: string[];
}

export function DataListWithFilters({ data, categories }: Props) {
  const { filterState, filteredData, updateFilter } = useFilteredData(data);

  return (
    <div className="data-list-container">
      {/* Filter Controls */}
      <aside className="filters-panel">
        <h3>Filters</h3>

        {/* Search */}
        <div className="filter-group">
          <label>Search</label>
          <input
            type="text"
            placeholder="Search products..."
            value={filterState.search || ''}
            onChange={(e) => updateFilter({ search: e.target.value })}
            className="search-input"
          />
        </div>

        {/* Category Checkboxes */}
        <div className="filter-group">
          <label>Category</label>
          {categories.map(cat => (
            <label key={cat} className="checkbox-label">
              <input
                type="checkbox"
                checked={filterState.category?.includes(cat) || false}
                onChange={(e) => {
                  const newCats = e.target.checked
                    ? [...(filterState.category || []), cat]
                    : filterState.category?.filter(c => c !== cat) || [];
                  updateFilter({ category: newCats });
                }}
              />
              {cat}
            </label>
          ))}
        </div>

        {/* Price Range Slider */}
        <div className="filter-group">
          <label>Price Range: ${filterState.priceRange?.[0] || 0} - ${filterState.priceRange?.[1] || 1000}</label>
          <input
            type="range"
            min="0"
            max="1000"
            value={filterState.priceRange?.[0] || 0}
            onChange={(e) => {
              const min = parseInt(e.target.value);
              const max = filterState.priceRange?.[1] || 1000;
              updateFilter({ priceRange: [min, max] });
            }}
          />
          <input
            type="range"
            min="0"
            max="1000"
            value={filterState.priceRange?.[1] || 1000}
            onChange={(e) => {
              const max = parseInt(e.target.value);
              const min = filterState.priceRange?.[0] || 0;
              updateFilter({ priceRange: [min, max] });
            }}
          />
        </div>

        {/* Clear Filters */}
        <button
          onClick={() => updateFilter({
            search: undefined,
            category: undefined,
            priceRange: undefined,
          })}
          className="btn-clear"
        >
          Clear Filters
        </button>
      </aside>

      {/* Main Content */}
      <main className="data-list-main">
        {/* Sort Controls */}
        <div className="sort-controls">
          <select
            value={filterState.sortBy || 'relevance'}
            onChange={(e) => updateFilter({ sortBy: e.target.value })}
          >
            <option value="relevance">Relevance</option>
            <option value="price">Price</option>
            <option value="rating">Rating</option>
            <option value="name">Name</option>
          </select>

          <button
            onClick={() => updateFilter({
              sortOrder: filterState.sortOrder === 'asc' ? 'desc' : 'asc'
            })}
            className="btn-sort-order"
          >
            {filterState.sortOrder === 'asc' ? '↑ Ascending' : '↓ Descending'}
          </button>

          <span className="result-count">
            {filteredData.length} results
          </span>
        </div>

        {/* Data List */}
        <div className="data-grid">
          {filteredData.length > 0 ? (
            filteredData.map(item => (
              <div key={item.id} className="data-card">
                <h4>{item.name}</h4>
                <p className="category">{item.category}</p>
                <p className="price">${item.price}</p>
                <p className="rating">⭐ {item.rating}/5</p>
              </div>
            ))
          ) : (
            <p className="no-results">No items match your filters</p>
          )}
        </div>
      </main>
    </div>
  );
}
```

### 4. CSS Styling

```css
/* styles.css */
.data-list-container {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 2rem;
  padding: 2rem;
  max-width: 1400px;
  margin: 0 auto;
}

.filters-panel {
  background: #f5f5f5;
  padding: 1.5rem;
  border-radius: 8px;
  height: fit-content;
  position: sticky;
  top: 2rem;
}

.filter-group {
  margin-bottom: 1.5rem;
}

.filter-group label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #333;
}

.search-input,
.filter-group input[type="range"] {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 0.9rem;
}

.checkbox-label {
  display: flex;
  align-items: center;
  margin: 0.5rem 0;
  cursor: pointer;
}

.checkbox-label input {
  margin-right: 0.5rem;
}

.btn-clear {
  width: 100%;
  padding: 0.75rem;
  background: #ff6b6b;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: background 0.2s;
}

.btn-clear:hover {
  background: #ff5252;
}

.sort-controls {
  display: flex;
  gap: 1rem;
  margin-bottom: 2rem;
  align-items: center;
}

.sort-controls select,
.btn-sort-order {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: pointer;
  font-size: 0.9rem;
}

.result-count {
  margin-left: auto;
  color: #666;
  font-size: 0.9rem;
}

.data-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1.5rem;
}

.data-card {
  background: white;
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 1.5rem;
  transition: box-shadow 0.2s;
}

.data-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.data-card h4 {
  margin: 0 0 0.5rem 0;
  color: #333;
}

.category {
  color: #999;
  font-size: 0.85rem;
  margin: 0.25rem 0;
}

.price {
  font-size: 1.25rem;
  font-weight: 700;
  color: #2ecc71;
  margin: 0.5rem 0;
}

.rating {
  color: #f39c12;
  margin: 0;
}

.no-results {
  grid-column: 1 / -1;
  text-align: center;
  color: #999;
  padding: 2rem;
}

@media (max-width: 768px) {
  .data-list-container {
    grid-template-columns: 1fr;
  }

  .filters-panel {
    position: static;
  }

  .data-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }
}
```

---

## Tips & Best Practices

### 1. **URL Encoding Best Practices**
```typescript
// ✅ DO: Use URLSearchParams for proper encoding
const params = new URLSearchParams();
params.set('search', 'laptop & phone'); // Handles special chars

// ❌ DON'T: Manual string concatenation
const url = `?search=${userInput}`; // Breaks with special chars
```

### 2. **Debounce Search Input**
```typescript
// Prevents excessive URL updates while typing
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout>();

const handleSearch = (value: string) => {
  clearTimeout(searchTimeout);
  setSearchTimeout(
    setTimeout(() => updateFilter({ search: value }), 300)
  );
};
```

### 3. **Preserve State on Navigation**
```typescript
// Use window.history.replaceState, not pushState
// This prevents back button spam
window.history.replaceState({ filterState }, '', newURL);
```

### 4. **Validate Query Parameters**
```typescript
// Sanitize and validate all query inputs
const validateCategory = (cat: string): boolean => {
  const validCategories = ['electronics', 'clothing', 'books'];
  return validCategories.includes(cat);
};
```

### 5. **Performance Optimization**
```typescript
// Memoize expensive filter operations
const filteredData = useMemo(() => {
  return applyFilters(filterState, allData);
}, [filterState, allData]);

// Virtualize long lists
import { FixedSizeList } from 'react-window';
```

### 6. **Accessibility**
```typescript
// Add ARIA labels and keyboard navigation
<input
  aria-label="Search products"
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
```

### 7. **Share Filter URLs**
```typescript
// Generate shareable links
const shareURL = `${window.location.origin}${window.location.pathname}?${queryString}`;
navigator.clipboard.writeText(shareURL);
```

### 8. **Handle Empty States**
```typescript
// Provide helpful messaging
{filteredData.length === 0 && (
  <div className="empty-state">
    <p>No results found</p>
    <button onClick={() => updateFilter({ search: undefined })}>
      Clear search
    </button>
  </div>
)}
```

---

## Summary

This implementation provides:
- ✅ Persistent filter state via URL query strings
- ✅ Shareable filtered views
- ✅ Smooth UX with debouncing
- ✅ Type-safe parameter handling
- ✅ Responsive, accessible UI
- ✅ Scalable architecture for complex filters
