Skip to main content

Filters and Sort

The Filters and Sort Service is a lightweight, framework-agnostic service for managing filter state, sorting, pagination, and search with automatic persistence and RxJS Observables for reactive updates.

Key Features

  • Zero-Dependency Core - Pure TypeScript, works everywhere
  • Type-Safe - Full TypeScript support with type inference
  • Simple State Management - Key-value filter objects and single-field sorting
  • Automatic Persistence - Built-in localStorage persistence with custom adapter support
  • Reactive Updates - RxJS Observable-based state management
  • Debounced State - Optional debouncing for performance optimization
  • Pagination Support - Built-in page/limit state management
  • Search Queries - Query string support for full-text search
  • Easy React Integration - Works seamlessly with React hooks and effects

Installation

# Install the filters and sort package
npm install @codella-software/utils

# Or from JSR
npx jsr add @codella-software/utils

Quick Start

Basic Filtering (Core)

import { FiltersAndSortService } from '@codella-software/utils';

// Define filter interface
interface UserFilters {
role?: string;
name?: string;
}

// Create filter service
const filterService = new FiltersAndSortService<UserFilters>({
storageKey: 'user-filters',
defaultFilters: { role: '', name: '' }
});

// Subscribe to filter state changes
filterService.getState().subscribe(state => {
console.log('Current filters:', state.filters);
console.log('Current sort:', state.sort);
console.log('Current pagination:', state.pagination);
});

// Update filters
filterService.setFilters({ role: 'User' });

// Add/update another filter
filterService.setFilters({ name: 'B' });

// Combined filters are automatically merged
const currentState = filterService.getCurrentState();
console.log('Applied filters:', currentState.filters);
// Output: { role: 'User', name: 'B' }

// Remove specific filter
filterService.removeFilter('name');

// Clear all filters
filterService.clearFilters();

Basic Filtering (React)

import { useFilters } from '@codella-software/react';

export function UserFilter() {
const {
filteredData,
setFilters,
removeFilter,
filters,
clearFilters
} = useFilters<{ role: string }>({
role: ''
});

return (
<div>
<button onClick={() => setFilters({ role: 'Admin' })}>
Admin Only
</button>
<button onClick={() => setFilters({ role: 'User' })}>
Users Only
</button>
<button onClick={clearFilters}>Clear Filters</button>


<ul>
{filteredData.map(user => (
<li key={user.id}>{user.name} ({user.role})</li>
))}
</ul>
</div>
);
}

Service Methods

The FiltersAndSortService provides simple state management for filters, sorting, and pagination:

Setting Filters

interface Filters {
status?: string;
category?: string;
dateRange?: [string, string];
}

const service = new FiltersAndSortService<Filters>({
storageKey: 'my-filters',
defaultFilters: { status: '', category: '' }
});

// Set or update filters (merged with existing)
service.setFilters({
status: 'active',
category: 'electronics'
});

// Get current filters
const { filters } = service.getCurrentState();
console.log(filters); // { status: 'active', category: 'electronics' }

Removing and Clearing Filters

// Remove specific filter
service.removeFilter('category');

// Clear all filters (reset to defaults)
service.clearFilters();

// Get state after changes
const state = service.getCurrentState();
console.log(state.filters); // Reflects changes

Sorting

// Set sort by field
service.setSort('name');

// Get current sort
const { sort } = service.getCurrentState();
console.log(sort); // { fieldName: 'name', direction: 'asc' }

// Toggle sort direction by calling again
service.setSort('name'); // Changes to 'desc'
service.setSort('name'); // Clears sort

// Clear sort
service.setSort('');

Search/Query

// Set search query
service.setQuery('john');

// Get current query
const { query } = service.getCurrentState();
console.log(query); // 'john'

// Clear query
service.setQuery('');

Pagination

// Set pagination
service.setPagination(1, 20); // page, limit

// Get current pagination
const { pagination } = service.getCurrentState();
console.log(pagination); // { page: 1, limit: 20 }

Practical Examples

Filter Multiple Fields

interface ProductFilters {
category?: string;
priceMin?: number;
priceMax?: number;
inStock?: boolean;
}

const filterService = new FiltersAndSortService<ProductFilters>({
storageKey: 'product-filters',
defaultFilters: {
category: '',
priceMin: undefined,
priceMax: undefined,
inStock: false
}
});

// Apply multiple filters at once
filterService.setFilters({
category: 'Electronics',
priceMin: 100,
priceMax: 500,
inStock: true
});

// Update just one filter (others remain)
filterService.setFilters({ priceMin: 200 });
// Result: category='Electronics', priceMin=200, priceMax=500, inStock=true

Date Range Filtering

interface DateFilters {
startDate?: string;
endDate?: string;
}

const filterService = new FiltersAndSortService<DateFilters>({
storageKey: 'date-filters',
defaultFilters: { startDate: '', endDate: '' }
});

const today = new Date().toISOString().split('T')[0];
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];

filterService.setFilters({
startDate: sevenDaysAgo,
endDate: today
});

// When filtering arrays, your UI/application logic handles date comparison

Search with Filters

const filterService = new FiltersAndSortService<{ category?: string }>({
storageKey: 'search-filters',
defaultFilters: { category: '' }
});

// Set search query (full-text search in UI layer)
filterService.setQuery('laptop');

// Also filter by category
filterService.setFilters({ category: 'Electronics' });

// Get current state
const state = filterService.getCurrentState();
console.log(state.query); // 'laptop'
console.log(state.filters); // { category: 'Electronics' }

Sorting

The service manages sort state with a single-field sort at a time. Clicking the same field twice toggles between ascending, descending, and no sort.

Basic Sorting

import { FiltersAndSortService } from '@codella-software/utils';

const sortService = new FiltersAndSortService<{ name?: string }>({
storageKey: 'sort-state',
defaultFilters: {}
});

// Sort by name ascending
sortService.setSort('name');
// State: { fieldName: 'name', direction: 'asc' }

// Click again - toggles to descending
sortService.setSort('name');
// State: { fieldName: 'name', direction: 'desc' }

// Click again - clears sort
sortService.setSort('name');
// State: undefined

// Clear sort explicitly
sortService.setSort('');
// State: undefined

// Subscribe to sort changes
sortService.getState().subscribe(state => {
console.log('Current sort:', state.sort);
// Output: { fieldName: 'name', direction: 'asc' }
});

Sorting with Pagination

interface DataFilters {
status?: string;
}

const service = new FiltersAndSortService<DataFilters>({
storageKey: 'table-state',
defaultFilters: { status: '' }
});

// Apply filters
service.setFilters({ status: 'active' });

// Set sort
service.setSort('createdDate');

// Go to page 1 when sorting
service.setPagination(1, 20);

// Get all state at once
const state = service.getCurrentState();
console.log({
filters: state.filters, // { status: 'active' }
sort: state.sort, // { fieldName: 'createdDate', direction: 'asc' }
pagination: state.pagination // { page: 1, limit: 20 }
});

React Integration

Using the Service Directly

The easiest way to use FiltersAndSortService in React is with the service directly:

import { useEffect, useState } from 'react';
import { FiltersAndSortService, FilterAndSortState } from '@codella-software/utils';

interface UserFilters {
department?: string;
status?: string;
}

export function UserTable() {
const [service] = useState(
() => new FiltersAndSortService<UserFilters>({
storageKey: 'users-table',
defaultFilters: { department: '', status: '' }
})
);

const [state, setState] = useState<FilterAndSortState<UserFilters>>(() =>
service.getCurrentState()
);

useEffect(() => {
const subscription = service.getState().subscribe(setState);
return () => subscription.unsubscribe();
}, [service]);

const handleDepartmentFilter = (dept: string) => {
service.setFilters({ department: dept });
};

const handleStatusFilter = (status: string) => {
service.setFilters({ status });
};

const handleClearFilters = () => {
service.clearFilters();
};

return (
<div>
<div className="filters">
<button onClick={() => handleDepartmentFilter('Engineering')}>
Engineering
</button>
<button onClick={() => handleDepartmentFilter('Sales')}>
Sales
</button>
<button onClick={() => handleStatusFilter('active')}>
Active Only
</button>
<button onClick={handleClearFilters}>Clear Filters</button>
</div>

<table>
<thead>
<tr>
<th onClick={() => service.setSort('name')}>Name</th>
<th onClick={() => service.setSort('department')}>Department</th>
<th onClick={() => service.setSort('createdDate')}>Created</th>
</tr>
</thead>
<tbody>
{/* Render filtered and sorted data */}
</tbody>
</table>

{/* Show active filters */}
<div className="active-filters">
<p>Active Filters: {JSON.stringify(state.filters)}</p>
<p>Sort: {state.sort ? `${state.sort.fieldName} (${state.sort.direction})` : 'None'}</p>
</div>
</div>
);
}

With useEffect for Advanced State Sync

import { useEffect, useState, useCallback } from 'react';
import { FiltersAndSortService } from '@codella-software/utils';

export function AdvancedFilterTable() {
const [service] = useState(
() => new FiltersAndSortService({
storageKey: 'advanced-table',
defaultFilters: {}
})
);

const [filters, setFilters] = useState({});
const [sort, setSort] = useState(undefined);
const [pagination, setPagination] = useState({ page: 0, limit: 20 });

useEffect(() => {
const subscription = service.getState().subscribe(state => {
setFilters(state.filters);
setSort(state.sort);
setPagination(state.pagination);
});
return () => subscription.unsubscribe();
}, [service]);

const updateFilters = useCallback((updates) => {
service.setFilters(updates);
}, [service]);

const updateSort = useCallback((fieldName) => {
service.setSort(fieldName);
}, [service]);

const goToPage = useCallback((page) => {
service.setPagination(page, pagination.limit);
}, [service, pagination]);

return (
<div>
{/* Filter controls using setFilters */}
{/* Sort headers using setSort */}
{/* Pagination using setPagination */}
</div>
);
}

State Management Pattern

Debounced State Updates

For responsive UIs with heavy filtering, debounce state updates:

import { useEffect, useState } from 'react';
import { FiltersAndSortService } from '@codella-software/utils';

export function OptimizedTable() {
const [service] = useState(
() => new FiltersAndSortService({
storageKey: 'optimized-table',
defaultFilters: {},
storageDebounceMs: 300 // Debounce storage writes
})
);

const [state, setState] = useState(() => service.getCurrentState());

useEffect(() => {
// Use debounced state for UI updates
const subscription = service.getDebouncedState(300).subscribe(setState);
return () => subscription.unsubscribe();
}, [service]);

return (
<div>
{/* Your table/list implementation */}
</div>
);
}

API Reference

FiltersAndSortService Methods

MethodDescription
setFilters(filters)Merge new filters with existing ones
removeFilter(key)Remove a specific filter
clearFilters()Clear all filters (reset to defaults)
setSort(fieldName)Set sort by field (toggles direction on repeated calls)
setQuery(query)Set search query string
setPagination(page, limit)Set pagination (1-based page number)
getState()Get Observable of state changes
getCurrentState()Get current state synchronously
getDebouncedState(ms)Get debounced state Observable
reset()Reset to initial state
clearStorage()Clear persisted storage

State Shape

interface FilterAndSortState<T> {
filters: T; // Filter key-value pairs
sort?: {
fieldName: string; // Field name to sort by
direction: 'asc' | 'desc'; // Sort direction
};
pagination: {
page: number; // Current page (0-based)
limit: number; // Items per page
};
query: string; // Search query
}

Persistence

By default, service state is automatically persisted to localStorage using the provided storageKey. Disable with persistToStorage: false:

const service = new FiltersAndSortService({
storageKey: 'my-filters',
persistToStorage: false // Disable persistence
});

Use a custom storage adapter:

const customAdapter = {
getItem: (key) => { /* custom get */ },
setItem: (key, value) => { /* custom set */ },
removeItem: (key) => { /* custom remove */ }
};

const service = new FiltersAndSortService({
storageKey: 'my-filters',
storageAdapter: customAdapter
});

Next Steps