The useUrlHashState hook provides a simple way to synchronize component state with the URL hash fragment, enabling deep linking, state persistence, and browser navigation support in documentation and demo pages.
The hook synchronizes state with the URL hash, enabling shareable links and browser navigation.
'use client';
import * as React from 'react';
import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
import styles from './TabNavigation.module.css';
const tabs = [
{
id: 'overview',
label: 'Overview',
content: 'Overview content - describing the main features.',
},
{ id: 'details', label: 'Details', content: 'Detailed information about the implementation.' },
{ id: 'examples', label: 'Examples', content: 'Code examples and usage patterns.' },
];
export function TabNavigation() {
const [hash, setHash] = useUrlHashState();
const activeTab = hash || 'overview';
const activeContent = tabs.find((tab) => tab.id === activeTab)?.content || tabs[0].content;
return (
<div className={styles.container}>
<div className={styles.tabs}>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setHash(tab.id, false)}
className={activeTab === tab.id ? styles.tabActive : styles.tab}
>
{tab.label}
</button>
))}
</div>
<div className={styles.content}>
<p className={styles.contentText}>{activeContent}</p>
<p className={styles.hint}>Try clicking tabs and using browser back/forward buttons</p>
</div>
</div>
);
}
import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
function TabNavigation() {
const [hash, setHash] = useUrlHashState();
return (
<nav>
<button onClick={() => setHash('overview')} className={hash === 'overview' ? 'active' : ''}>
Overview
</button>
<button onClick={() => setHash('details')} className={hash === 'details' ? 'active' : ''}>
Details
</button>
<button onClick={() => setHash(null)}>Clear Hash</button>
</nav>
);
}
---
## Common Patterns
### Tab Navigation
Simple tab navigation with URL synchronization.
```tsx
function NavigationExample() {
const [hash, setHash] = useUrlHashState();
const goToSection = (section: string, addToHistory = false) => {
// Use replace=false to add entry to browser history
// Use replace=true (default) to replace current entry
setHash(section, !addToHistory);
};
return (
<div>
return (
<div>
<button type="button" onClick={() => goToSection('intro', true)}>
Go to Intro (new history entry)
</button>
<button type="button" onClick={() => goToSection('content')}>
Go to Content (replace current)
</button>
</div>
);
}
Store complex state as JSON in the URL hash.
import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
function DemoPage() {
const [hash, setHash] = useUrlHashState();
// Parse hash as JSON for complex state
const demoState = React.useMemo(() => {
if (!hash) return { variant: 'default', size: 'medium' };
try {
return JSON.parse(decodeURIComponent(hash));
} catch {
return { variant: 'default', size: 'medium' };
}
}, [hash]);
const updateDemoState = (updates: Partial<typeof demoState>) => {
const newState = { ...demoState, ...updates };
setHash(encodeURIComponent(JSON.stringify(newState)));
};
return (
<div>
<Demo config={demoState} />
<Controls state={demoState} onChange={updateDemoState} />
</div>
);
}
Automatically scroll to sections when hash changes.
import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
function SectionNavigator() {
const [hash, setHash] = useUrlHashState();
React.useEffect(() => {
if (hash) {
const element = document.getElementById(hash);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}, [hash]);
return (
<nav>
<button type="button" onClick={() => setHash('section1')}>
Go to Section 1
</button>
<button type="button" onClick={() => setHash('section2')}>
Go to Section 2
</button>
</nav>
);
}
Enable deep links to modals or overlays.
import { useUrlHashState } from '@mui/internal-docs-infra/useUrlHashState';
function ModalExample() {
const [hash, setHash] = useUrlHashState();
const isModalOpen = hash === 'modal';
return (
<div>
<button type="button" onClick={() => setHash('modal')}>
Open Modal
</button>
{isModalOpen && (
<Modal onClose={() => setHash(null)}>
<p>Modal content - shareable URL!</p>
</Modal>
)}
</div>
);
}
Control whether to add entries to browser history.
function DocumentationPage() {
const [hash, setHash] = useUrlHashState();
const sections = ['introduction', 'api', 'examples', 'troubleshooting'];
const activeSection = hash || 'introduction';
return (
<div>
<nav>
{sections.map((section) => (
<button
key={section}
onClick={() => setHash(section)}
className={activeSection === section ? 'active' : ''}
>
{section}
</button>
))}
</nav>
<main>
{activeSection === 'introduction' && <IntroductionContent />}
{activeSection === 'api' && <ApiContent />}
{activeSection === 'examples' && <ExamplesContent />}
{activeSection === 'troubleshooting' && <TroubleshootingContent />}
</main>
</div>
);
}
function DemoPage() {
const [hash, setHash] = useUrlHashState();
// Parse hash as JSON for complex state
const demoState = React.useMemo(() => {
if (!hash) return { variant: 'default', size: 'medium' };
try {
return JSON.parse(decodeURIComponent(hash));
} catch {
return { variant: 'default', size: 'medium' };
}
}, [hash]);
const updateDemoState = (updates: Partial<typeof demoState>) => {
const newState = { ...demoState, ...updates };
setHash(encodeURIComponent(JSON.stringify(newState)));
};
return (
<div>
<Demo config={demoState} />
<Controls state={demoState} onChange={updateDemoState} />
</div>
);
}
Use the fragment delimiter convention for structured navigation.
function HierarchicalNavigation() {
const [hash, setHash] = useUrlHashState();
// Parse hierarchical fragments like "api:props" or "demos:button:outlined"
const segments = hash?.split(':') || [];
const [section, subsection, variant] = segments;
return (
<div>
{/* Primary navigation */}
<nav>
<button
type="button"
onClick={() => setHash('api')}
className={section === 'api' ? 'active' : ''}
>
API
</button>
<button
type="button"
onClick={() => setHash('demos')}
className={section === 'demos' ? 'active' : ''}
>
Demos
</button>
</nav>
{/* Secondary navigation within API section */}
{section === 'api' && (
<nav>
<button
type="button"
onClick={() => setHash('api:props')}
className={subsection === 'props' ? 'active' : ''}
>
Props
</button>
<button
type="button"
onClick={() => setHash('api:methods')}
className={subsection === 'methods' ? 'active' : ''}
>
Methods
</button>
</nav>
)}
{/* Demo variants */}
{section === 'demos' && subsection === 'button' && (
<nav>
<button
type="button"
onClick={() => setHash('demos:button:outlined')}
className={variant === 'outlined' ? 'active' : ''}
>
Outlined
</button>
<button
type="button"
onClick={() => setHash('demos:button:contained')}
className={variant === 'contained' ? 'active' : ''}
>
Contained
</button>
</nav>
)}
</div>
);
}
const [hash, setHash] = useUrlHashState();
A tuple containing:
| Index | Type | Description |
|---|---|---|
0 | string | null | Current hash value (without the '#' prefix) |
1 | (value: string | null, replace?: boolean) => void | Function to update hash and URL |
| Parameter | Type | Default | Description |
|---|---|---|---|
value | string | null | - | New hash value to set, or null to clear the hash |
replace | boolean | true | Whether to use replaceState (true) or pushState (false) |
The hook uses useSyncExternalStore for synchronization:
window.location.hash on mounthashchange events for browser navigationhistory.replaceState() or history.pushState() to update URLnull and skips URL operations on the serverWhen NOT to use:
useLocalStorageState - Alternative for private state persistence