
The Origin Story
It started with a bookmark manager.
While building Luma Bookmarks—a folder-based bookmark organizer with Google Drive sync—I needed a way to handle folder navigation. Users would click into nested folders, and I wanted that navigation to feel native: back button works, refresh preserves position, and shareable links that open exactly where you left off.
The solution? Treat the URL as state.
I built a small hook that synced folder paths with the URL. It worked beautifully. But as the codebase grew, I noticed something: this pattern was solving more than just navigation. It was handling search filters, modal states, pagination—anything that should survive a refresh or be shareable.
So I extracted it. Polished it. Published it.

The folder navigation system in Luma Bookmarks. Every folder path syncs with the URL, enabling deep-linking and browser history support.
The Philosophy
What if the URL was more than just an address?
Every time you share a link, you're sharing a snapshot of state. The tab you have open. The filter you applied. The search term you typed. The browser has always known this—URLs survive refreshes, work with back/forward navigation, and can be copied into a message.
But React apps treat the URL like an afterthought. State lives in useState, dies with the tab, and can't be shared.
I wanted to bridge that gap.
The Problem
Syncing React state with URL search params is surprisingly tedious:
- Boilerplate everywhere — The same
URLSearchParamsparsing logic repeated across components - Prop drilling hell — Multiple components need the same URL state? Time to lift state up
- Router lock-in — Most solutions tie you to React Router or Next.js
- Hydration nightmares — Server-side rendering introduces subtle mismatches
- Re-render chaos — Updating one param shouldn't re-render components watching other params
For something so fundamental, there had to be a simpler way.
The Solution
One hook. Familiar API. Zero dependencies.
import { useAddressState } from "use-address-state";
function SearchPage() {
const [query, setQuery] = useAddressState("q");
return (
<input
value={query || ""}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}That's it. Now query is synced with ?q=... in the URL. Update it, and the URL updates. Share the link, and anyone opening it gets the same state.
How It Works
The magic is in three pieces:
1. Reads from the URL
Using window.location.search and URLSearchParams, the hook parses the current value for your key.
2. Writes with History API
Updates use history.pushState() directly—no router dependency, no page navigation, just clean URL updates.
3. Syncs with useSyncExternalStore
React 18's useSyncExternalStore ensures tear-free reads and proper concurrent mode support. Components subscribed to different keys won't re-render each other.
Selective Re-rendering
Here's where it gets interesting. Components only re-render when their specific key changes:
function CounterA() {
const [count, setCount] = useAddressState("a", 0);
// Only re-renders when 'a' changes
return <button onClick={() => setCount(count + 1)}>A: {count}</button>;
}
function CounterB() {
const [count, setCount] = useAddressState("b", 0);
// Only re-renders when 'b' changes
return <button onClick={() => setCount(count + 1)}>B: {count}</button>;
}Click button A a hundred times—button B sits quietly, untouched.
Shared State Without Prop Drilling
Components using the same key automatically share state:
// header.tsx
function SearchBar() {
const [query, setQuery] = useAddressState("q");
return <input value={query || ""} onChange={(e) => setQuery(e.target.value)} />;
}
// results.tsx
function SearchResults() {
const [query] = useAddressState("q");
return <div>Showing results for: {query}</div>;
}No context providers. No props. No Redux. Just the URL as your single source of truth.
Perfect For
- Search inputs and filters — Persist and share search queries
- Pagination —
?page=3that survives refreshes - Tab and accordion state — Deep-linkable UI sections
- Modal states —
?modal=settingsfor shareable dialogs - Form wizards —
?step=2for multi-step flows - Any UI state worth sharing — If it should survive a refresh, put it in the URL
The Constraints
Being honest about limitations:
- Client-side only — The URL is a browser concept; SSR gets initial values but updates happen client-side
- String-based storage — Values are JSON-serialized; not ideal for complex objects
- URL length limits — Keep it under ~2000 characters for browser compatibility
This isn't meant to replace all state management. It's for the specific case where you want state in the URL.
The Technical Details
| Metric | Value |
|---|---|
| Bundle Size | ~1KB minified |
| Dependencies | Zero |
| React Version | 18.0.0+ |
| TypeScript | Full support |
| SSR | Safe with fallbacks |
Works with Create React App, Vite, Next.js, Remix—anything that runs React in a browser.
Try It
Live Demo
Experience the library in action with interactive examples.
View on npm
Check out the package statistics, versions, and installation guide.
Source on GitHub
Explore the source code, contribute, or open an issue.
pnpm add use-address-state