@matthew.ngo/react-filterize
is a React library that provides hooks and utility functions to easily manage and synchronize filter states across URLs, local storage, and API calls.
View Demo here
# Using npm
npm install @matthew.ngo/react-filterize
# Using yarn
yarn add @matthew.ngo/react-filterize
# Using pnpm
pnpm add @matthew.ngo/react-filterize
import { useFilterize, addFilter, ValueTypes } from '@matthew.ngo/react-filterize';
// 1. Define your filters
const config = [
addFilter({
key: 'search',
type: ValueTypes.STRING
}),
addFilter({
key: 'category',
type: ValueTypes.STRING
}),
addFilter({
key: 'minPrice',
type: ValueTypes.NUMBER
})
];
// 2. Create your fetch function
const fetchData = async (filters) => {
const response = await api.get('/products', { params: filters });
return response.data;
};
// 3. Use the hook
function ProductList() {
const {
filters,
updateFilter,
data,
loading
} = useFilterize({
config,
fetch: fetchData
});
return (
<div>
<input
value={filters.search || ''}
onChange={(e) => updateFilter('search', e.target.value)}
/>
{/* Rest of your UI */}
</div>
);
}
- Use Meaningful Keys: Choose filter keys that reflect their purpose
// Good
addFilter({ key: 'categoryId', type: ValueTypes.NUMBER })
// Avoid
addFilter({ key: 'f1', type: ValueTypes.NUMBER })
- Provide Default Values When Appropriate:
addFilter({
key: 'status',
type: ValueTypes.STRING,
defaultValue: 'active'
})
- Add Labels and Descriptions:
addFilter({
key: 'dateRange',
type: ValueTypes.DATE_ARRAY,
label: 'Date Range',
description: 'Select start and end dates'
})
- Use Debouncing for Text Filters:
const options = {
fetch: {
debounceTime: 300 // 300ms delay
}
}
- Implement Caching:
const options = {
cacheTimeout: 5000, // Cache results for 5 seconds
storage: {
type: 'local',
key: 'product-filters'
}
}
- Selective URL Synchronization:
const options = {
url: {
include: ['search', 'category'], // Only sync these filters
exclude: ['temporaryFilter'] // Never sync these
}
}
function BasicExample() {
const config = [
addFilter({
key: 'search',
type: ValueTypes.STRING
})
];
const { filters, updateFilter } = useFilterize({
config,
fetch: async (filters) => {
// Your fetch logic
}
});
return (
<input
value={filters.search || ''}
onChange={(e) => updateFilter('search', e.target.value)}
/>
);
}
function AdvancedExample() {
const config = [
addFilter({
key: 'search',
type: ValueTypes.STRING,
transform: (value) => value.toLowerCase()
}),
addFilter({
key: 'price',
type: ValueTypes.NUMBER_ARRAY,
dependencies: {
currency: async (value) => {
// Convert price based on currency
return value;
}
}
})
];
const options = {
url: {
key: 'f',
encode: true
},
storage: {
type: 'local',
version: '1.0.0',
migrations: [
{
fromVersion: '0.9.0',
transform: (data) => {
// Migration logic
return data;
}
}
]
},
fetch: {
debounceTime: 300,
requiredFilters: ['category'],
beforeFetch: (filters) => {
// Transform filters before fetch
return filters;
}
}
};
const { filters, updateFilter } = useFilterize({
config,
fetch: async (filters) => {
// Your fetch logic
},
options
});
return (
// Your UI
);
}
All default values are defined in the core configuration. Here's a comprehensive breakdown of each option and its default value:
Option | Type | Default | Description |
---|---|---|---|
cacheTimeout |
number |
300000 (5 minutes) |
How long to cache results in milliseconds |
autoFetch |
boolean |
true |
Whether to automatically fetch when filters change |
Controls how filters are synchronized with the URL.
Option | Type | Default | Description |
---|---|---|---|
key |
string |
'filters' |
Prefix for URL parameters |
encode |
boolean |
true |
Whether to encode/decode URL parameters |
mergeParams |
boolean |
true |
Whether to merge with existing URL params (true) or override them (false) |
namespace |
string |
'ogn-filters' |
Namespace for multiple filter instances on same page |
transformers |
Record<string, (value: any) => string> |
{} |
Custom transform functions for specific filter keys |
Example usage:
const options = {
url: {
key: 'myFilters',
namespace: 'products',
transformers: {
date: (value: Date) => value.toISOString()
}
}
}
Controls how filters are persisted in storage.
Option | Type | Default | Description |
---|---|---|---|
type |
'local' | 'session' | 'memory' | 'none' |
'none' |
Storage type to use |
key |
string |
- | Storage key (required if type !== 'none') |
version |
string |
- | Data schema version |
migrations |
MigrationStrategy[] |
- | Migration strategies for version upgrades |
include |
string[] |
- | Fields to include in storage |
exclude |
string[] |
- | Fields to exclude from storage |
compress |
boolean |
false |
Whether to compress stored data |
serialize |
(data: any) => string |
- | Custom serialization function |
deserialize |
(data: string) => any |
- | Custom deserialization function |
Example usage:
const options = {
storage: {
type: 'local',
key: 'product-filters',
version: '1.0.0',
migrations: [{
fromVersion: '0.9.0',
transform: (data) => {
// Migration logic
return data;
}
}]
}
}
Controls retry behavior for failed requests.
Option | Type | Default | Description |
---|---|---|---|
attempts |
number |
3 |
Number of retry attempts |
delay |
number |
1000 |
Delay between retries in milliseconds |
backoff |
boolean |
true |
Whether to use exponential backoff |
Example usage:
const options = {
retry: {
attempts: 5,
delay: 2000,
backoff: true
}
}
Controls data transformation before and after fetching.
Option | Type | Default | Description |
---|---|---|---|
input |
(data: any) => any |
(data) => data |
Transform filters before fetching |
output |
(data: any) => any |
(data) => data |
Transform response data after fetching |
Example usage:
const options = {
transform: {
input: (filters) => ({
...filters,
timestamp: Date.now()
}),
output: (data) => data.map(item => ({
...item,
price: Number(item.price)
}))
}
}
Controls the fetching behavior.
Option | Type | Default | Description |
---|---|---|---|
dependencies |
any[] |
[] |
Additional dependencies that trigger refetch |
debounceTime |
number |
300 |
Debounce time in milliseconds |
fetchOnEmpty |
boolean |
true |
Whether to fetch when filters are empty |
requiredFilters |
string[] |
[] |
Filters that must have values before fetching |
shouldFetch |
(filters: Record<string, any>) => boolean | Promise<boolean> |
() => true |
Condition to determine if fetch should occur |
beforeFetch |
(filters: Record<string, any>) => Record<string, any> | Promise<Record<string, any>> |
filters => filters |
Transform filters right before fetching |
onMissingRequired |
(missingFilters: string[]) => void |
() => {} |
Called when required filters are missing |
onFetchPrevented |
(filters: Record<string, any>) => void |
() => {} |
Called when shouldFetch returns false |
Example usage:
const options = {
fetch: {
debounceTime: 500,
requiredFilters: ['category', 'search'],
shouldFetch: (filters) => Object.keys(filters).length > 0,
beforeFetch: async (filters) => {
const enrichedFilters = await enrichFilters(filters);
return enrichedFilters;
},
onMissingRequired: (missing) => {
console.warn('Missing required filters:', missing);
}
}
}
Here's an example showing all options with their default values:
const options = {
// Base options
cacheTimeout: 5 * 60 * 1000, // 5 minutes
autoFetch: true,
// URL synchronization
url: {
key: 'filters',
encode: true,
mergeParams: true,
namespace: 'ogn-filters',
transformers: {}
},
// Storage configuration
storage: {
type: 'none' as const,
key: undefined,
version: undefined,
migrations: undefined,
include: undefined,
exclude: undefined,
compress: false,
serialize: undefined,
deserialize: undefined
},
// Retry configuration
retry: {
attempts: 3,
delay: 1000,
backoff: true
},
// Data transformation
transform: {
input: (data: any) => data,
output: (data: any) => data
},
// Fetch behavior
fetch: {
dependencies: [],
debounceTime: 300,
fetchOnEmpty: true,
requiredFilters: [],
shouldFetch: () => true,
beforeFetch: filters => filters,
onMissingRequired: () => {},
onFetchPrevented: () => {}
}
};
For TypeScript users, here are the complete type definitions for the options:
interface UseFilterizeOptions<TConfig extends FilterConfig[]> {
url?: UrlConfig | boolean;
storage?: StorageConfig;
cacheTimeout?: number;
autoFetch?: boolean;
retry?: RetryConfig;
transform?: TransformConfig;
fetch?: FetchConfig;
defaults?: DefaultValuesConfig;
}
interface UrlConfig {
key?: string;
encode?: boolean;
mergeParams?: boolean;
namespace?: string;
transformers?: Record<string, (value: any) => string>;
serialize?: (filters: Record<string, any>) => string;
deserialize?: (query: string) => Record<string, any>;
}
interface StorageConfig {
type?: 'local' | 'session' | 'memory' | 'none';
key?: string;
version?: string;
migrations?: MigrationStrategy[];
include?: string[];
exclude?: string[];
compress?: boolean;
serialize?: (data: any) => string;
deserialize?: (data: string) => any;
onMigrationComplete?: (oldVersion: string, newVersion: string, data: any) => void;
}
interface RetryConfig {
attempts: number;
delay: number;
backoff?: boolean;
}
interface TransformConfig {
input?: (data: any) => any;
output?: (data: any) => any;
}
interface FetchConfig {
dependencies?: any[];
debounceTime?: number;
fetchOnEmpty?: boolean;
requiredFilters?: string[];
shouldFetch?: (filters: Record<string, any>) => boolean | Promise<boolean>;
beforeFetch?: (filters: Record<string, any>) => Record<string, any> | Promise<Record<string, any>>;
onMissingRequired?: (missingFilters: string[]) => void;
onFetchPrevented?: (filters: Record<string, any>) => void;
}
- Filters Not Updating
- Check if
updateFilter
is called with correct types - Verify filter configuration matches expected values
- Check browser console for errors
- URL Sync Issues
- Ensure URL configuration is correct
- Check if filters are included/excluded properly
- Verify URL encoding settings
- Storage Problems
- Clear local storage and retry
- Check storage version and migrations
- Verify storage configuration
Q: Can I use this with REST and GraphQL APIs?
A: Yes, the fetch function is API-agnostic. You can implement any data fetching logic.
Q: Does it support SSR?
A: Yes, the library is SSR-friendly when configured properly.
Q: How does caching work?
A: Results are cached based on filter values for the duration specified in cacheTimeout
.
Q: How can I optimize performance with large datasets?
A:
- Use appropriate debounce times
- Implement server-side pagination
- Cache results when possible
- Only sync necessary filters to URL
Q: What's the recommended way to handle multiple filter instances?
A: Use unique namespaces in URL and storage configurations:
const options = {
url: {
namespace: 'productFilters'
},
storage: {
key: 'product-filters'
}
}
Q: How can I implement custom serialization?
A: Use the serialize
and deserialize
options:
const options = {
url: {
serialize: (filters) => {
// Custom serialization logic
return JSON.stringify(filters);
},
deserialize: (query) => {
// Custom deserialization logic
return JSON.parse(query);
}
}
}
Q: Can I transform data before/after fetch?
A: Yes, use the transform options:
const options = {
transform: {
input: (filters) => {
// Transform before fetch
return filters;
},
output: (data) => {
// Transform after fetch
return data;
}
}
}