Updating nested state in Zustand
Zustand state is updated immutably, so changing a value deep inside an object means building new copies of each level above it.
How it works
- A deeply nested state object. The store keeps
deep.nested.obj.countplus a sibling fielddeep.title. Both actions only touch the count. [1] - Copy each level with spread.
normalIncspreadsstate.deep, thenstate.deep.nested, thenstate.deep.nested.obj, and writes the new count at the bottom. [1] - Immutable state model. Because a new object is built each time, the JSON view shows the new count while
deep.titleis carried over unchanged. [2]
Run and edit it
It runs in your browser, in a sandboxed frame that cannot see this site; the packages load from esm.sh only after you press Run.
import { create } from 'zustand'
const useDeepStore = create((set) => ({
deep: { nested: { obj: { count: 0 } }, title: 'sibling field, left as is' },
// Copy each level with the spread operator, then change the leaf
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: { ...state.deep.nested.obj, count: state.deep.nested.obj.count + 1 },
},
},
})),
resetCount: () =>
set((state) => ({
deep: { ...state.deep, nested: { ...state.deep.nested, obj: { count: 0 } } },
})),
}))
const btn = { padding: '9px 16px', border: 0, borderRadius: 8, cursor: 'pointer', fontWeight: 600 }
export default function App() {
const deep = useDeepStore((state) => state.deep)
const count = useDeepStore((state) => state.deep.nested.obj.count)
const normalInc = useDeepStore((state) => state.normalInc)
const resetCount = useDeepStore((state) => state.resetCount)
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#111', fontFamily: 'system-ui, sans-serif' }}>
<div style={{ background: '#1c1f26', color: '#e8eaf0', padding: 26, borderRadius: 14, width: 400, boxSizing: 'border-box' }}>
<h2 style={{ margin: '0 0 10px', fontSize: 20 }}>deep.nested.obj.count = {count}</h2>
<pre style={{ background: '#12141a', color: '#a8d8ff', padding: 12, borderRadius: 8, fontSize: 12, margin: '0 0 14px' }}>
{JSON.stringify({ deep }, null, 2)}
</pre>
<div style={{ display: 'flex', gap: 10 }}>
<button style={{ ...btn, background: '#f5b84a', color: '#1a1a1a' }} onClick={normalInc}>normalInc()</button>
<button style={{ ...btn, background: '#2f343f', color: '#e8eaf0' }} onClick={resetCount}>resetCount()</button>
</div>
</div>
</div>
)
}
Press normalInc() and watch the count change in the JSON while the sibling field stays the same. Code written by AgentGid [1].
Press normalInc() and watch the count change in the JSON while the sibling field stays the same. Code written by AgentGid [1].Checked Oct 12, 2026: it starts without errors in Chromium with zustand 5.0.15, react 19.3.0, react-dom 19.3.0. Needs WebGL.
More Zustand examples
Creating a store in Zustand Updating state from inputs in Zustand Async actions in Zustand Using a Zustand store outside components
Sources
Explanations written by AgentGid from the official docs; the numbers in brackets point here. The code marked "written by AgentGid" is ours and free to copy.