# Immutable state updates

Zustand keeps state immutable: update through the store API, and return new references for the parts that changed. The default update is a shallow merge; nested values and complete-state replacement are explicit choices.

## How the update works

The [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) function gives a React component a bound store hook. Its initializer receives `set`, which accepts a partial object or an updater function. Zustand compares the updater result with the current state using `Object.is`; when the result differs, it either shallowly merges the object or replaces the state.

```mermaid
flowchart LR
  A["set(updater)"] --> B["next state"]
  B --> C{"Object.is(next, current)?"}
  C -->|yes| D["no notification"]
  C -->|no| E{"replace = true?"}
  E -->|no| F["shallow merge one level"]
  E -->|yes| G["replace complete state"]
  F --> H["notify subscribers"]
  G --> H
```

The reference check is about the state value returned by the updater. For an object nested inside that state, mutate-in-place can leave the nested reference unchanged, so return a new nested object instead.

## Shallow updates

Use a partial object for a flat update. The other top-level properties remain in the store:

```tsx
import { create } from 'zustand'

type PersonState = {
  firstName: string
  lastName: string
  updateFirstName: (firstName: string) => void
}

const usePersonStore = create<PersonState>()((set) => ({
  firstName: 'Ada',
  lastName: 'Lovelace',
  updateFirstName: (firstName) => set({ firstName }),
}))

export function PersonEditor() {
  const firstName = usePersonStore((state) => state.firstName)
  const lastName = usePersonStore((state) => state.lastName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)

  return (
    <main>
      <label>
        First name
        <input
          value={firstName}
          onChange={(event) => updateFirstName(event.currentTarget.value)}
        />
      </label>
      <p>{firstName} {lastName}</p>
    </main>
  )
}
```

Typing in the input updates `firstName` while `lastName` stays in the state. The component selects each value separately, so each selected primitive changes only when that value changes.

## Nested updates

The merge stops at the first level. Preserve each object on the path to the field you change:

```tsx
import { create } from 'zustand'

type Settings = {
  theme: string
  density: 'comfortable' | 'compact'
}

type SettingsState = {
  settings: Settings
  setTheme: (theme: string) => void
}

const useSettingsStore = create<SettingsState>()((set) => ({
  settings: { theme: 'light', density: 'comfortable' },
  setTheme: (theme) =>
    set((state) => ({
      settings: { ...state.settings, theme },
    })),
}))

export function SettingsPanel() {
  const settings = useSettingsStore((state) => state.settings)
  const setTheme = useSettingsStore((state) => state.setTheme)

  return (
    <section>
      <p>Theme: {settings.theme}</p>
      <p>Density: {settings.density}</p>
      <button type="button" onClick={() => setTheme('dark')}>
        Use dark theme
      </button>
    </section>
  )
}
```

Clicking the button changes `theme` and keeps `density`. Omitting `...state.settings` would create a new `settings` object without `density`, because the default merge does not recursively merge nested objects. For deeper structures, copy every changed level, or use the [Immer update guide](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer).

## Replacing the complete state

Pass `true` as the second argument to `set` when the returned value is the complete state model. Replacement also removes properties that are not in the new value, including actions in a store that defines actions in its state.

This standalone example uses [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) from the [`zustand/vanilla`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-vanilla) package and binds it to React with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore):

```tsx
import { createStore } from 'zustand/vanilla'
import { useStore } from 'zustand'

type NoticeState = {
  message: string
  visible: boolean
}

const noticeStore = createStore<NoticeState>(() => ({
  message: 'Saved',
  visible: true,
}))

export function Notice() {
  const notice = useStore(noticeStore)

  return (
    <section>
      <p>{notice.visible ? notice.message : 'No notice'}</p>
      <button
        type="button"
        onClick={() =>
          noticeStore.setState(
            { message: 'Archived', visible: false },
            true,
          )
        }
      >
        Archive
      </button>
    </section>
  )
}
```

Before the click, the component shows `Saved`. After the click, replacement changes the complete `NoticeState` value and the component shows `No notice`. Use replacement only when you have every property required by the state type.

## Collections and references

`Map` and `Set` are mutable objects, so create a new instance when changing one. The new instance gives the selected value a new reference:

```ts
import { createStore } from 'zustand/vanilla'

type TagState = {
  tags: Set<string>
}

const tagStore = createStore<TagState>(() => ({ tags: new Set<string>() }))

const addTag = (tag: string) => {
  tagStore.setState((state) => ({
    tags: new Set(state.tags).add(tag),
  }))
}
```

Do not mutate `state.tags` and return the same `Set`. Its reference remains equal, so a selector of `tags` does not observe a changed reference. Type empty collections explicitly when TypeScript cannot infer their element types.

## Next steps

- [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) explains how selected references affect re-renders.
- [Update nested state with Immer](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer) covers the middleware and its pitfalls.
- [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) develops the standalone-store pattern.
