# Sync state with a URL hash

Use [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with a custom [`StateStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#statestorage) adapter and [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#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`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#createjsonstorage). The `name` is the key inside the hash, so choose a unique name for the store.

```tsx title="src/fish-store.ts"
import { 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.

```tsx title="src/App.tsx"
import { 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.

```ts title="src/query-storage.ts"
import 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`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#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](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) for the `createJSONStorage`, `JSON.parse`, and `JSON.stringify` details.
- If you use asynchronous persisted storage elsewhere, hydration can finish after the initial render. See [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data).

## Live demos

- [Hash storage demo](https://stackblitz.com/edit/vitejs-vite-9vg24prg)
- [Query-parameter demo](https://stackblitz.com/edit/vitejs-vite-hyc97ynf)

## Related

- [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data)
- [Persist selected state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-selected-state)
- [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration)
- [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores)
