Skip to main content

Filter Synchronization

The url-filter-sync module (lib/utils/url-filter-sync.ts) handles bidirectional synchronization between filter state and URL parameters. It supports clean URLs for single-filter routes, query parameter encoding for multi-filter combinations, and optional location and search filters.

Core Types

FilterState

The central state object representing all active filters:

interface FilterState {
tags: string[];
categories: string[];
q?: string; // Search query (optional)
nearLat?: number; // Latitude for proximity search
nearLng?: number; // Longitude for proximity search
radius?: number; // Search radius
city?: string; // City filter
country?: string; // Country filter
}

URLFilterOptions

Configuration for URL generation:

interface URLFilterOptions {
basePath?: string; // Default: "/"
locale?: string; // Locale prefix (e.g., "en", "fr")
}

URL Generation

generateFilterURL

Generates a URL string from the current filter state. The function applies smart routing rules to produce clean URLs when possible:

import { generateFilterURL } from '@/lib/utils/url-filter-sync';

// No filters: base path
generateFilterURL({ tags: [], categories: [] });
// "/"

// Single tag: clean URL
generateFilterURL({ tags: ['react'], categories: [] });
// "/tags/react"

// Single category: clean URL
generateFilterURL({ tags: [], categories: ['tools'] });
// "/categories/tools"

// Multiple tags: query parameters
generateFilterURL({ tags: ['react', 'vue'], categories: [] });
// "/?tags=react,vue"

// Mixed filters: query parameters
generateFilterURL({ tags: ['react'], categories: ['tools'] });
// "/?tags=react&categories=tools"

URL Routing Rules

ConditionURL PatternExample
No filtersBase path/
Single tag, no categoriesClean tag URL/tags/react
Single category, no tagsClean category URL/categories/tools
Multiple tags or categoriesQuery parameters/?tags=a,b&categories=x,y

Locale Support

When a locale is provided, it is prepended to all generated URLs:

generateFilterURL(
{ tags: ['react'], categories: [] },
{ locale: 'fr' }
);
// "/fr/tags/react"

generateFilterURL(
{ tags: ['react', 'vue'], categories: [] },
{ locale: 'en', basePath: '/directory' }
);
// "/en/directory?tags=react,vue"

Location and Search Parameters

Location and search parameters are appended as query strings to any URL pattern:

generateFilterURL({
tags: ['react'],
categories: [],
q: 'dashboard',
nearLat: 40.7128,
nearLng: -74.006,
radius: 50,
});
// "/tags/react?q=dashboard&near_lat=40.7128&near_lng=-74.006&radius=50"

generateFilterURL({
tags: [],
categories: [],
city: 'New York',
country: 'US',
});
// "/?city=New+York&country=US"

URL Parsing

parseFilterFromSearchParams

Extracts filter state from URL search parameters:

import { parseFilterFromSearchParams } from '@/lib/utils/url-filter-sync';

const params = new URLSearchParams('tags=react,vue&categories=tools&q=search');
const filters = parseFilterFromSearchParams(params);
// {
// tags: ['react', 'vue'],
// categories: ['tools'],
// q: undefined,
// ...
// }

The function also parses location parameters:

const params = new URLSearchParams(
'near_lat=40.7128&near_lng=-74.006&radius=50&city=New+York'
);
const filters = parseFilterFromSearchParams(params);
// {
// tags: [],
// categories: [],
// nearLat: 40.7128,
// nearLng: -74.006,
// radius: 50,
// city: 'New York',
// }

parseFilterFromTagRoute

Creates a filter state from a tag route parameter:

import { parseFilterFromTagRoute } from '@/lib/utils/url-filter-sync';

// For route /tags/project-management
const filters = parseFilterFromTagRoute('project-management');
// { tags: ['project-management'], categories: [] }

parseFilterFromCategoryRoute

Creates a filter state from a category route parameter:

import { parseFilterFromCategoryRoute } from '@/lib/utils/url-filter-sync';

// For route /categories/team-tools
const filters = parseFilterFromCategoryRoute('team-tools');
// { tags: [], categories: ['team-tools'] }

Filter State Manipulation

The module provides immutable helper functions for modifying filter state. Each function returns a new FilterState object rather than mutating the input.

Adding Filters

import {
addTagToFilters,
addCategoryToFilters,
} from '@/lib/utils/url-filter-sync';

const state = { tags: ['react'], categories: [] };

addTagToFilters(state, 'vue');
// { tags: ['react', 'vue'], categories: [] }

addCategoryToFilters(state, 'tools');
// { tags: ['react'], categories: ['tools'] }

Duplicate additions are ignored:

addTagToFilters(state, 'react');
// { tags: ['react'], categories: [] } -- unchanged

Removing Filters

import {
removeTagFromFilters,
removeCategoryFromFilters,
} from '@/lib/utils/url-filter-sync';

const state = { tags: ['react', 'vue'], categories: ['tools'] };

removeTagFromFilters(state, 'vue');
// { tags: ['react'], categories: ['tools'] }

removeCategoryFromFilters(state, 'tools');
// { tags: ['react', 'vue'], categories: [] }

Toggling Filters

Toggle functions add a filter if absent, or remove it if present:

import {
toggleTagInFilters,
toggleCategoryInFilters,
} from '@/lib/utils/url-filter-sync';

const state = { tags: ['react'], categories: [] };

toggleTagInFilters(state, 'react');
// { tags: [], categories: [] } -- removed

toggleTagInFilters(state, 'vue');
// { tags: ['react', 'vue'], categories: [] } -- added

Clearing All Filters

import { clearAllFilters } from '@/lib/utils/url-filter-sync';

clearAllFilters();
// { tags: [], categories: [] }

Filter Comparison

isFilterEmpty

Checks whether any filters are active:

import { isFilterEmpty } from '@/lib/utils/url-filter-sync';

isFilterEmpty({ tags: [], categories: [] }); // true
isFilterEmpty({ tags: ['react'], categories: [] }); // false
isFilterEmpty({ tags: [], categories: [], city: 'NYC' }); // false

The function also considers location filters (city, country, coordinates) as non-empty.

areFiltersEqual

Performs a deep comparison of two filter states including location data:

import { areFiltersEqual } from '@/lib/utils/url-filter-sync';

const a = { tags: ['react'], categories: [], nearLat: 40.7 };
const b = { tags: ['react'], categories: [], nearLat: 40.7 };
areFiltersEqual(a, b); // true

const c = { tags: ['react', 'vue'], categories: [] };
const d = { tags: ['react'], categories: [] };
areFiltersEqual(c, d); // false

Note that tag and category order matters for this comparison -- ['react', 'vue'] is not equal to ['vue', 'react'].

Encoding and Decoding

Filter values are encoded for URL safety using encodeURIComponent and decoded with decodeURIComponent:

// Internal helper functions
function encodeFilterValue(value: string): string {
return encodeURIComponent(value.toLowerCase().trim());
}

function decodeFilterValue(value: string): string {
try {
return decodeURIComponent(value);
} catch (error) {
console.error('Error decoding filter value:', error);
return value; // Fallback to raw value on decode errors
}
}

Values are lowercased and trimmed during encoding. Decoding includes error handling to gracefully handle malformed URL values.

Typical Integration Pattern

A common pattern in page components:

import {
parseFilterFromSearchParams,
generateFilterURL,
toggleTagInFilters,
} from '@/lib/utils/url-filter-sync';

function FilterableList({ searchParams }: { searchParams: URLSearchParams }) {
const filters = parseFilterFromSearchParams(searchParams);

const handleTagClick = (tag: string) => {
const newFilters = toggleTagInFilters(filters, tag);
const newUrl = generateFilterURL(newFilters);
router.push(newUrl);
};

return (
<div>
<TagList
tags={availableTags}
activeTags={filters.tags}
onTagClick={handleTagClick}
/>
<ItemGrid filters={filters} />
</div>
);
}

Source Files

FilePurpose
lib/utils/url-filter-sync.tsURL filter synchronization utilities