@withtahmid/use-url-state
v1.0.6
Published
Lifts react-state to URL
Readme
useURLState
A React hook that seamlessly syncs state with URL query parameters using base64 encoding. Perfect for shareable filters, pagination, search parameters, and any state you want to persist in the URL.
Features
✨ Simple API - Works just like useState
🔗 URL Sync - Automatically syncs state with URL parameters
🔄 Browser Navigation - Supports back/forward buttons
🎯 TypeScript - Full type safety out of the box
🌐 SSR Compatible - Works with Next.js and other SSR frameworks
⚡ Debouncing - Optional debouncing for performance
🎨 Customizable - Custom serialization and encoding options
📦 Lightweight - Zero dependencies (except React)
Installation
npm install @withtahmid/use-url-stateyarn add @withtahmid/use-url-statepnpm add @withtahmid/use-url-stateQuick Start
import { useURLState } from '@withtahmid/use-url-state';
function SearchPage() {
const [filters, setFilters] = useURLState('filters', {
search: '',
category: 'all',
page: 1
});
return (
<div>
<input
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
/>
{/* URL automatically updates: ?filters=eyJzZWFyY2giOiJ0ZXN0In0 */}
</div>
);
}API
Basic Usage
const [state, setState] = useURLState<T>(key, defaultValue, options?);Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| key | string | Yes | The URL query parameter key |
| defaultValue | T | Yes | Default value when parameter is not present |
| options | UseURLStateOptions<T> | No | Configuration options |
Options
interface UseURLStateOptions<T> {
/** Custom serializer function. Defaults to JSON.stringify */
serialize?: (value: T) => string;
/** Custom deserializer function. Defaults to JSON.parse */
deserialize?: (value: string) => T;
/** Maximum URL length before warning. Defaults to 2000 */
maxURLLength?: number;
/** Callback when URL length exceeds maxURLLength */
onURLLengthExceeded?: (length: number) => void;
/** Debounce delay in ms for URL updates. Defaults to 0 (no debounce) */
debounceMs?: number;
}Examples
Simple Search Filter
import { useURLState } from '@withtahmid/use-url-state';
function ProductList() {
const [search, setSearch] = useURLState('q', '');
return (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search products..."
/>
);
}Complex Filter State
import { useURLState } from '@withtahmid/use-url-state';
interface Filters {
category: string;
priceRange: [number, number];
inStock: boolean;
sortBy: 'price' | 'name' | 'date';
}
function FilteredList() {
const [filters, setFilters] = useURLState<Filters>('filters', {
category: 'all',
priceRange: [0, 1000],
inStock: false,
sortBy: 'name'
});
const updateCategory = (category: string) => {
setFilters({ ...filters, category });
};
return (
<div>
<select value={filters.category} onChange={(e) => updateCategory(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
</div>
);
}With Debouncing
import { useURLState } from '@withtahmid/use-url-state';
function LiveSearch() {
const [query, setQuery] = useURLState('search', '', {
debounceMs: 500 // Wait 500ms before updating URL
});
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
/>
);
}Custom Serialization
function DateRangePicker() {
const [dateRange, setDateRange] = useURLState(
'dates',
{ start: new Date(), end: new Date() },
{
serialize: (value) => `${value.start.toISOString()},${value.end.toISOString()}`,
deserialize: (str) => {
const [start, end] = str.split(',');
return { start: new Date(start), end: new Date(end) };
}
}
);
return <div>{/* Your date picker UI */}</div>;
}URL Length Monitoring
function LargeStateExample() {
const [data, setData] = useURLState('data', [], {
maxURLLength: 2000,
onURLLengthExceeded: (length) => {
alert(`URL too long (${length} chars). Consider reducing data size.`);
}
});
return <div>{/* Your component */}</div>;
}Pagination
function PaginatedTable() {
const [page, setPage] = useURLState('page', 1);
return (
<div>
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)}>
Next
</button>
</div>
);
}How It Works
- Encoding: State is serialized to JSON, then encoded using URL-safe base64
- URL Sync: The encoded value is stored as a query parameter
- Decoding: When reading from URL, the process is reversed
- Navigation: Browser back/forward buttons automatically restore previous states
URL Format
Given a state object like { search: "test", page: 2 }:
Before: https://example.com/products
After: https://example.com/products?filters=eyJzZWFyY2giOiJ0ZXN0IiwicGFnZSI6Mn0Important Considerations
URL Length Limits
Browsers have URL length limits (typically ~2000 characters). For large state objects:
- Use the
maxURLLengthoption to monitor URL size - Consider storing large data in a database and only keeping IDs in the URL
- Use custom serialization to compress data
Server-Side Rendering
The hook is SSR-compatible and will use defaultValue during server rendering. State will hydrate from the URL on the client side.
// ✅ Works in Next.js
export default function Page() {
const [filters, setFilters] = useURLState('filters', { search: '' });
return <div>{/* ... */}</div>;
}Default Value Behavior
When state equals defaultValue, the URL parameter is removed to keep URLs clean:
const [count, setCount] = useURLState('count', 0);
setCount(0); // Parameter removed from URL
setCount(5); // URL becomes ?count=NQ (base64 for "5")TypeScript Support
Full TypeScript support with type inference:
interface UserPreferences {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
// Type is automatically inferred
const [prefs, setPrefs] = useURLState<UserPreferences>('prefs', {
theme: 'light',
language: 'en',
notifications: true
});
// TypeScript will catch errors
setPrefs({ theme: 'blue' }); // ❌ Error: 'blue' is not assignable to 'light' | 'dark'Browser Support
Works in all modern browsers that support:
URLSearchParamshistory.replaceStatebtoa/atob
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © withtahmid
Links
Made with ❤️ by withtahmid
