useQueryParamsState
State Management
A React hook that manages state in URL query parameters, perfect for shareable and bookmarkable UI states.
Demo
These filters are stored in URL query parameters and can be shared or bookmarked:
Installation
npm install @thibault.sh/hooks
Usage
import { useQueryParamsState } from '@thibault.sh/hooks/useQueryParamsState';
function SearchPage() {
const [searchParams, setSearchParams] = useQueryParamsState('q', {
query: '',
page: 1,
limit: 10
});
return (
<div>
<input
value={searchParams.query}
onChange={(e) => setSearchParams(prev => ({
...prev,
query: e.target.value,
page: 1 // Reset page when query changes
}))}
placeholder="Search..."
/>
<div>
Page {searchParams.page} of results
<button onClick={() => setSearchParams(prev => ({
...prev,
page: prev.page + 1
}))}>
Next Page
</button>
</div>
</div>
);
}
API
Parameters
key
stringThe query parameter key
initialValue
TThe initial value to use if the parameter doesn't exist
options
ObjectConfiguration options
options.serialize
(value: T) => stringFunction to convert value to string (default: JSON.stringify)
options.deserialize
(value: string) => TFunction to parse string back to value (default: JSON.parse)
Returns
A tuple containing the current value and a setter function
Custom Serialization
// Example with custom serialization for dates
const [dateRange, setDateRange] = useQueryParamsState(
'range',
{
start: new Date(),
end: new Date()
},
{
serialize: (value) => ({
start: value.start.toISOString(),
end: value.end.toISOString()
}),
deserialize: (value) => ({
start: new Date(value.start),
end: new Date(value.end)
})
}
);
Features
- ✓URL Persistence
State is stored in the URL, making it shareable and bookmarkable
- ✓Custom Serialization
Support for custom serialization and deserialization of complex data types
- ✓Type Safety
Full TypeScript support with generics
- ✓Browser History Integration
Works seamlessly with browser history and navigation
Table View Example
import { useQueryParamsState } from '@thibault.sh/hooks';
interface TableState {
page: number;
pageSize: number;
sortColumn: string;
sortDirection: 'asc' | 'desc';
filters: Record<string, string>;
}
function DataTable() {
const [tableState, setTableState] = useQueryParamsState<TableState>(
'table',
{
page: 1,
pageSize: 10,
sortColumn: 'id',
sortDirection: 'asc',
filters: {}
}
);
const handleSort = (column: string) => {
setTableState(prev => ({
...prev,
sortColumn: column,
sortDirection: prev.sortColumn === column && prev.sortDirection === 'asc'
? 'desc'
: 'asc'
}));
};
const handleFilter = (column: string, value: string) => {
setTableState(prev => ({
...prev,
page: 1, // Reset to first page when filtering
filters: {
...prev.filters,
[column]: value
}
}));
};
return (
<div>
{/* Table implementation */}
</div>
);
}
Map View Example
import { useQueryParamsState } from '@thibault.sh/hooks';
interface MapViewState {
center: {
lat: number;
lng: number;
};
zoom: number;
layers: string[];
}
function MapView() {
const [mapState, setMapState] = useQueryParamsState<MapViewState>(
'map',
{
center: { lat: 0, lng: 0 },
zoom: 2,
layers: ['terrain']
},
{
// Custom serialization to make URLs cleaner
serialize: (value) => ({
c: `${value.center.lat},${value.center.lng}`,
z: value.zoom,
l: value.layers.join(',')
}),
deserialize: (value) => ({
center: {
lat: parseFloat(value.c.split(',')[0]),
lng: parseFloat(value.c.split(',')[1])
},
zoom: parseInt(value.z),
layers: value.l.split(',')
})
}
);
const handleMapMove = (newCenter: { lat: number; lng: number }) => {
setMapState(prev => ({
...prev,
center: newCenter
}));
};
const toggleLayer = (layer: string) => {
setMapState(prev => ({
...prev,
layers: prev.layers.includes(layer)
? prev.layers.filter(l => l !== layer)
: [...prev.layers, layer]
}));
};
return (
<div>
{/* Map implementation */}
</div>
);
}