Sync state with a URL hash
Use create with a custom StateStorage adapter and persist when a shareable URL must restore selected Zustand state. This example stores a fish count in the URL hash and renders the count in React.
When to use this
Use this pattern for small, shareable state such as a filter, selected tab, or view setting. The storage adapter keeps the URL representation separate from the store; persist still performs serialization and rehydration.
Store the state in the hash
Create the hash adapter, then pass it through createJSONStorage. The name is the key inside the hash, so choose a unique name for the store.
tsximport { create } from 'zustand'
import {
createJSONStorage,
persist,
type StateStorage,
} from 'zustand/middleware'
type FishStore = {
fishes: number
addAFish: () => void
}
const hashStorage: StateStorage = {
getItem: (key) => {
const searchParams = new URLSearchParams(window.location.hash.slice(1))
return searchParams.get(key)
},
setItem: (key, value) => {
const searchParams = new URLSearchParams(window.location.hash.slice(1))
searchParams.set(key, value)
window.location.hash = searchParams.toString()
},
removeItem: (key) => {
const searchParams = new URLSearchParams(window.location.hash.slice(1))
searchParams.delete(key)
window.location.hash = searchParams.toString()
},
}
export const useFishStore = create<FishStore>()(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'fish-store',
storage: createJSONStorage<FishStore>(() => hashStorage),
},
),
)
createJSONStorage writes the persisted envelope as JSON. After an update, the hash contains an encoded fish-store value with the state and its persistence version. A new page load reads that value before the component displays the restored count.
Render and update it in React
Read the state and action through the store hook. Mounting this component gives you a visible count and a button; clicking the button updates both the count and the URL hash.
tsximport { useFishStore } from './fish-store'
export function App() {
const fishes = useFishStore((state) => state.fishes)
const addAFish = useFishStore((state) => state.addAFish)
return (
<main>
<p>Fishes: {fishes}</p>
<button type="button" onClick={addAFish}>
Add a fish
</button>
</main>
)
}
The component re-renders when its selected fishes value changes. Copy the page URL after adding fish, open it in a new tab, and the store rehydrates the count from the hash.
Use a query parameter instead
A query-string adapter has the same StateStorage shape. Use history.replaceState in setItem so updating the state changes the URL without navigating or refreshing the page. The adapter below reads the store's persisted value from ?fish-store=... and writes it in place.
tsimport type { StateStorage } from 'zustand/middleware'
export const queryStorage: StateStorage = {
getItem: (key) => {
return new URLSearchParams(window.location.search).get(key)
},
setItem: (key, value) => {
const searchParams = new URLSearchParams(window.location.search)
searchParams.set(key, value)
window.history.replaceState(
null,
'',
`${window.location.pathname}?${searchParams.toString()}${window.location.hash}`,
)
},
removeItem: (key) => {
const searchParams = new URLSearchParams(window.location.search)
searchParams.delete(key)
const query = searchParams.toString()
window.history.replaceState(
null,
'',
`${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`,
)
},
}
Replace the storage option in the store with createJSONStorage(() => queryStorage). The rest of the store and component remain unchanged. If the query string is empty, this adapter leaves it empty until the first persisted update.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
name | string | — | Names the persisted value and must be unique. |
storage | PersistStorage | createJSONStorage(() => window.localStorage) | Selects the storage implementation used to read, write, and remove persisted state. |
partialize | (state: Object) => Object | (state) => state | Selects which fields go into the URL. |
version | number | 0 | Identifies the persisted state format. |
migrate | (persistedState: Object, version: number) => Object | Promise<Object> | (persistedState) => persistedState | Converts a persisted value from an older version. |
Pitfalls
- URL values are visible and shareable. Persist only state that is safe to put in a URL.
- The URL adapter makes persisted values visible and shareable, so validate untrusted or stale URL data before treating it as store state. See Persist store data for the
createJSONStorage,JSON.parse, andJSON.stringifydetails. - If you use asynchronous persisted storage elsewhere, hydration can finish after the initial render. See Persist store data.
Was this page helpful?