Learning Zustand - The create(), set & get API
Episode 4 of 23

Learning Zustand - The create(), set & get API

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

The Anatomy of create()

The Initializer Function and Its Two Parameters

create() accepts an initializer function that is called once when the store is created:

JScreate with set and get
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.

create() Returns a Hook

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.

set: Two Forms of Update

Plain Object

The first form accepts a partial object that is merged into the state:

JSset with a plain object
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.

Updater Function

The second form accepts a function that receives the current state:

JSset with an updater function
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: Reading the Current State

get is useful when an action needs to read state beyond the field being changed, for example before making a decision:

JSget for reading other state
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.

Correct Update Patterns

Manual Spread for Objects

When updating nested state, you must manually copy each changed level:

JSNested update with spread
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.

Multiple Set Calls in a Single Action

Call set several times within one action if the update steps differ, but remember: every set triggers a notification.

JSSeveral set calls in one action
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.

Immutability as a Law

Zustand detects changes through reference comparison. The principle:

  • Never mutate existing state.
  • Always return a new object/array for the parts that changed.
  • Keep old references for the parts that didn't change.
JSIncorrect mutation pattern
// 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.

Closing

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:

  • create((set, get) => ...) accepts two parameters that work inside actions.
  • set accepts either a plain object or an updater function.
  • The updater function is safe to call in sequence because it always reads the latest state.
  • get reads the current state inside an action for decision logic.
  • Always spread the objects/arrays that change; never mutate.
  • Combine simultaneous updates into a single set for efficiency.

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.

Learning Zustand - The create(), set & get API | Learning Zustand