The useUrlState hook synchronizes component state with URL query parameters using Next.js navigation. It enables shareable links, preserves filter states across page refreshes, and maintains component state in the browser’s navigation history.
Import
import { useUrlState } from "@/hooks" ;
Signature
function useUrlState (
key : string ,
defaultValue ?: string
) : readonly [ string , ( newValue : string ) => void ]
Parameters
The URL query parameter key to synchronize with. This becomes the parameter name in the URL (e.g., "severity" → ?severity=critical).
The default value when the URL parameter is not present.
Return Value
Returns a tuple similar to React’s useState:
The current value from the URL query parameter, or defaultValue if not present.
setValue
(newValue: string) => void
Function to update the URL parameter. Pass an empty string to remove the parameter from the URL.
Usage Examples
Basic Filter State
Sync a filter dropdown with the URL:
import { useUrlState } from "@/hooks" ;
function AssetFilters () {
const [ severity , setSeverity ] = useUrlState ( "criticality_level" , "" );
const [ siteId , setSiteId ] = useUrlState ( "site_id" , "" );
return (
< div >
< select value = { severity } onChange = { ( e ) => setSeverity ( e . target . value ) } >
< option value = "" > All Severities </ option >
< option value = "critical" > Critical </ option >
< option value = "high" > High </ option >
< option value = "medium" > Medium </ option >
</ select >
< select value = { siteId } onChange = { ( e ) => setSiteId ( e . target . value ) } >
< option value = "" > All Sites </ option >
{ sites . map (( site ) => (
< option key = { site . id } value = { site . id } >
{ site . name }
</ option >
)) }
</ select >
</ div >
);
}
URL result: /assets?criticality_level=critical&site_id=123
Tab Navigation
Persist active tab in the URL:
import { useUrlState } from "@/hooks" ;
function TabsContent () {
const [ tab , setTab ] = useUrlState ( "tab" , "roles" );
return (
< div >
< div className = "tabs" >
< button
className = { tab === "roles" ? "active" : "" }
onClick = { () => setTab ( "roles" ) }
>
Roles
</ button >
< button
className = { tab === "permissions" ? "active" : "" }
onClick = { () => setTab ( "permissions" ) }
>
Permissions
</ button >
</ div >
< div className = "content" >
{ tab === "roles" && < RolesContent /> }
{ tab === "permissions" && < PermissionsContent /> }
</ div >
</ div >
);
}
URL result: /settings?tab=permissions
Multiple Filters (Real-World Example)
From the MicroCBM recommendation filters:
import { useUrlState } from "@/hooks" ;
function RecommendationFilters ({ sites , assets , samplingPoints , users }) {
const [ searchSeverity , setSearchSeverity ] = useUrlState ( "severity" , "" );
const [ site_id , setSearchSiteId ] = useUrlState ( "site_id" , "" );
const [ asset_id , setSearchAssetId ] = useUrlState ( "asset_id" , "" );
const [ sampling_point_id , setSearchSamplingPointId ] = useUrlState (
"sampling_point_id" ,
""
);
const [ recommender_id , setSearchRecommenderId ] = useUrlState (
"recommender_id" ,
""
);
const clearFilters = () => {
setSearchSeverity ( "" );
setSearchSiteId ( "" );
setSearchAssetId ( "" );
setSearchSamplingPointId ( "" );
setSearchRecommenderId ( "" );
};
return (
< div >
{ /* Filter dropdowns */ }
< button onClick = { clearFilters } > Clear All Filters </ button >
</ div >
);
}
URL result: /recommendations?severity=critical&site_id=123&asset_id=456
Search with Modal State
Combine search and modal selection:
import { useUrlState } from "@/hooks" ;
function RoleCards () {
const [ searchName , setSearchName ] = useUrlState ( "name" , "" );
const [, setRoleId ] = useUrlState ( "roleId" , "" );
const handleRoleClick = ( roleId : string ) => {
setRoleId ( roleId ); // Opens modal with role details
};
return (
< div >
< input
type = "text"
value = { searchName }
onChange = { ( e ) => setSearchName ( e . target . value ) }
placeholder = "Search roles..."
/>
{ roles
. filter (( role ) => role . name . includes ( searchName ))
. map (( role ) => (
< RoleCard key = { role . id } onClick = { () => handleRoleClick ( role . id ) } />
)) }
</ div >
);
}
Manage page number in the URL:
import { useUrlState } from "@/hooks" ;
function PaginatedTable () {
const [ page , setPage ] = useUrlState ( "page" , "1" );
const currentPage = parseInt ( page ) || 1 ;
return (
< div >
< Table data = { getData ( currentPage ) } />
< Pagination
current = { currentPage }
onChange = { ( newPage ) => setPage ( String ( newPage )) }
/>
</ div >
);
}
URL result: /data?page=3
Clearing URL Parameters
Pass an empty string to remove a parameter:
import { useUrlState } from "@/hooks" ;
function FilterPanel () {
const [ filter , setFilter ] = useUrlState ( "filter" , "" );
return (
< div >
< input
value = { filter }
onChange = { ( e ) => setFilter ( e . target . value ) }
/>
< button onClick = { () => setFilter ( "" ) } > Clear Filter </ button >
</ div >
);
}
Behavior
URL Updates
Calling setValue updates the URL using router.replace(), which:
Updates the URL without creating a new history entry
Preserves other query parameters
Does not trigger a page refresh
Does not scroll the page ({ scroll: false })
Parameter Management
Setting a value : setValue("newValue") → URL contains ?key=newValue
Clearing a value : setValue("") → Removes the parameter from the URL
Other parameters : Existing query parameters are preserved
Initial Value
The hook reads the current URL parameter on mount and uses defaultValue if the parameter is absent.
Benefits
Shareable Links Users can copy and share URLs with active filters and state
Browser Navigation Back/forward buttons work correctly with state changes
Persistent State State survives page refreshes and external link navigation
Clean API Simple API identical to React’s useState
Notes
This hook uses router.replace() instead of router.push(), so URL changes don’t create new history entries. Each state change updates the current history entry.
Values are always strings. Convert to numbers or booleans as needed: const [ page , setPage ] = useUrlState ( "page" , "1" );
const pageNumber = parseInt ( page ) || 1 ;
This hook requires Next.js App Router and the "use client" directive. It relies on useRouter, usePathname, and useSearchParams from next/navigation.
Common Patterns
Multiple Filters with Reset
const [ filter1 , setFilter1 ] = useUrlState ( "f1" , "" );
const [ filter2 , setFilter2 ] = useUrlState ( "f2" , "" );
const [ filter3 , setFilter3 ] = useUrlState ( "f3" , "" );
const resetAll = () => {
setFilter1 ( "" );
setFilter2 ( "" );
setFilter3 ( "" );
};
Numeric Values
const [ pageStr , setPageStr ] = useUrlState ( "page" , "1" );
const page = parseInt ( pageStr ) || 1 ;
const setPage = ( num : number ) => setPageStr ( String ( num ));
Boolean Flags
const [ showArchived , setShowArchived ] = useUrlState ( "archived" , "" );
const isArchived = showArchived === "true" ;
const toggleArchived = () => setShowArchived ( isArchived ? "" : "true" );