Skip to content
intermediatePhase 37 · Frontend Architecture

Filtering

Implement client-side and server-side filtering with URL state synchronization.

45m
0 problems
Topic Progress0%

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

0/3solved
Build Filtering Component

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 needed
Filtering Testing

Write 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 tests
Filtering Performance

Optimize 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 analysis

Quiz

1. When should you use client-side filtering?

Question 1 options

2. Why sync filters with URL?

Question 2 options

3. How should you handle filter changes in server-side filtering?

Question 3 options

4. What is a filter preset?

Question 4 options

Flashcards

Question

What is the difference between client and server filtering?

Answer

Client filtering runs in the browser on loaded data; server filtering sends parameters to the API.

Question

Why debounce filter API calls?

Answer

To avoid overwhelming the server with requests while the user is still adjusting filters.

Question

How do you sync filters with URL?

Answer

Store filter values as URL search params and read them back on component mount.

Question

What are filter presets?

Answer

Predefined filter configurations that users can apply with one click.

Question

What is Filtering?

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);