This episode breaks down the anatomy of create((set, get) => ...), the two forms of set — a plain object and an updater function — and the role of get for reading the current state inside actions. You also learn correct update patterns with spread and immutability.

In episode 3 you wrote your first store without realizing the full power held by set and get. Episode 4 breaks down the anatomy of create(): what the initializer function receives, the two forms of set, when to use get, and why immutability is a non-negotiable law.
After this episode, you'll write complex actions — reading other state, updating several fields at once, and avoiding reference bugs — with confidence.
create() accepts an initializer function that is called once when the store is created:
import { create } from 'zustand'
type CartState = {
items: string[]
addItem: (item: string) => void
summary: () => number
}
export const useCart = create<CartState>((set, get) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
summary: () => get().items.length,
}))The first parameter set updates state, the second parameter get reads the current state. get().items.length inside summary reads the array length when the action is called — not when the store is created.
The result of create() is a hook with static properties: .getState(), .setState(), and .subscribe(). Store and hook are one, with no separate configuration — this is what makes the Zustand API feel like a single unit.
The first form accepts a partial object that is merged into the state:
reset: () => set({ count: 0, status: 'idle' })set({ count: 0, status: 'idle' }) replaces the count and status fields at once, while other fields are preserved. This form is appropriate when the new value is already available and doesn't depend on the old state.
The second form accepts a function that receives the current state:
increment: () => set((s) => ({ count: s.count + 1 }))set((s) => ({ count: s.count + 1 })) is safe to call repeatedly because it always reads the latest state. If several set calls run in sequence, the updater form won't lose changes — unlike a plain object, which is computed from the old value.
get is useful when an action needs to read state beyond the field being changed, for example before making a decision:
export const useAuth = create<AuthState>((set, get) => ({
user: null,
login: (user) => {
const prev = get().user
set({ user, lastLogin: prev ? prev.name : null })
},
isLoggedIn: () => get().user !== null,
}))
if (useAuth.getState().isLoggedIn()) {
console.log('sudah login')
}get().user inside an action reads the latest state, and useAuth.getState().isLoggedIn() makes it possible to call from outside a component — for example, from a utility or a router guard.
When updating nested state, you must manually copy each changed level:
type ProfileState = {
profile: { name: string; email: string }
setEmail: (email: string) => void
}
export const useProfile = create<ProfileState>((set) => ({
profile: { name: 'Arman', email: '' },
setEmail: (email) =>
set((s) => ({ profile: { ...s.profile, email } })),
})){ ...s.profile, email } creates a new profile object while preserving the other fields. Mutating s.profile.email = email without a copy will break change detection — this is the core topic of immer in episode 9.
Call set several times within one action if the update steps differ, but remember: every set triggers a notification.
export const useForm = create<FormState>((set) => ({
values: {},
errors: {},
submitStart: () => {
set({ status: 'loading' })
set({ errors: {} })
},
}))For updates that can be combined, prefer a single set with several fields. set({ status: 'loading', errors: {} }) is more efficient than two separate calls.
Zustand detects changes through reference comparison. The principle:
// BAD: direct mutation, not detected
set((s) => {
s.user.name = 'Baru'
return s
})The code above returns the same object, so the component won't re-render. Always create a new object like the spread pattern above, or use the immer middleware from episode 9 to write it safely.
Episode 4 completes your basic arsenal: create((set, get) => ...) builds the store, set updates state in two forms, get reads the current state, and immutability keeps change detection working.
Key takeaways:
In the next episode we will discuss selectors and subscriptions — choosing a state slice so re-renders stay minimal, using useShallow for several slices, and manual subscriptions with subscribeWithSelector for logging, analytics, and non-React synchronization.