Skip to main content
Syncing your Zustand state with the URL allows you to create shareable links that restore application state. This guide covers two approaches: URL hash and query parameters.

URL Hash Storage

Connect your store state to the URL hash using a custom storage implementation with the persist middleware.
1

Create hash storage

Implement a custom StateStorage that uses the URL hash:
import { create } from 'zustand'
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'

const hashStorage: StateStorage = {
  getItem: (key): string => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    const storedValue = searchParams.get(key) ?? ''
    return JSON.parse(storedValue)
  },
  setItem: (key, newValue): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.set(key, JSON.stringify(newValue))
    location.hash = searchParams.toString()
  },
  removeItem: (key): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.delete(key)
    location.hash = searchParams.toString()
  },
}
2

Use with persist middleware

Apply the custom storage to your store:
export const useBoundStore = create()(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // unique name
      storage: createJSONStorage(() => hashStorage),
    },
  ),
)
With hash storage, your state is stored in the URL after the # symbol. For example: https://example.com/#food-storage={"state":{"fishes":5}}

URL Query Parameters

For more control, you can sync state with URL query parameters while also persisting to localStorage.
1

Create hybrid storage

This storage checks URL params first, then falls back to localStorage:
import { create } from 'zustand'
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'

const getUrlSearch = () => {
  return window.location.search.slice(1)
}

const persistentStorage: StateStorage = {
  getItem: (key): string => {
    // Check URL first
    if (getUrlSearch()) {
      const searchParams = new URLSearchParams(getUrlSearch())
      const storedValue = searchParams.get(key)
      return JSON.parse(storedValue as string)
    } else {
      // Otherwise, load from localStorage
      return JSON.parse(localStorage.getItem(key) as string)
    }
  },
  setItem: (key, newValue): void => {
    // Update URL if query params exist
    if (getUrlSearch()) {
      const searchParams = new URLSearchParams(getUrlSearch())
      searchParams.set(key, JSON.stringify(newValue))
      window.history.replaceState(null, '', `?${searchParams.toString()}`)
    }

    // Always update localStorage
    localStorage.setItem(key, JSON.stringify(newValue))
  },
  removeItem: (key): void => {
    const searchParams = new URLSearchParams(getUrlSearch())
    searchParams.delete(key)
    window.location.search = searchParams.toString()
  },
}
2

Define your store

Create a store with the hybrid storage:
type LocalAndUrlStore = {
  typesOfFish: string[]
  addTypeOfFish: (fishType: string) => void
  numberOfBears: number
  setNumberOfBears: (newNumber: number) => void
}

const storageOptions = {
  name: 'fishAndBearsStore',
  storage: createJSONStorage<LocalAndUrlStore>(() => persistentStorage),
}

const useLocalAndUrlStore = create()(
  persist<LocalAndUrlStore>(
    (set) => ({
      typesOfFish: [],
      addTypeOfFish: (fishType) =>
        set((state) => ({ typesOfFish: [...state.typesOfFish, fishType] })),

      numberOfBears: 0,
      setNumberOfBears: (numberOfBears) => set(() => ({ numberOfBears })),
    }),
    storageOptions,
  ),
)
3

Generate shareable URLs

Create helper functions to build shareable links:
const buildURLSuffix = (params, version = 0) => {
  const searchParams = new URLSearchParams()

  const zustandStoreParams = {
    state: {
      typesOfFish: params.typesOfFish,
      numberOfBears: params.numberOfBears,
    },
    version: version, // Zustand includes version in persisted state
  }

  // Key must match the store name from storageOptions
  searchParams.set('fishAndBearsStore', JSON.stringify(zustandStoreParams))
  return searchParams.toString()
}

export const buildShareableUrl = (params, version) => {
  return `${window.location.origin}?${buildURLSuffix(params, version)}`
}
4

Use in components

Generate and share URLs with embedded state:
function ShareButton() {
  const store = useLocalAndUrlStore()

  const handleShare = () => {
    const url = buildShareableUrl({
      typesOfFish: store.typesOfFish,
      numberOfBears: store.numberOfBears,
    }, 0)

    navigator.clipboard.writeText(url)
    alert('Shareable URL copied to clipboard!')
  }

  return <button onClick={handleShare}>Share State</button>
}

Example URL Format

The generated URL would look like this (shown unencoded for readability):
https://localhost/search?fishAndBearsStore={"state":{"typesOfFish":["tilapia","salmon"],"numberOfBears":15},"version":0}
When someone visits this URL, the store automatically initializes with the embedded state.

Comparison

Pros:
  • Simple implementation
  • Doesn’t trigger page reloads
  • Changes don’t affect browser history
Cons:
  • Limited URL aesthetics
  • Only in URL, not persisted locally
  • Hash changes may interfere with routing

Live Demos

Hash Storage Demo

See URL hash storage in action

Query Parameters Demo

Explore query parameter syncing
Be mindful of URL length limits (approximately 2000 characters in most browsers). For large state objects, consider storing only essential data in the URL or using compression.

Build docs developers (and LLMs) love