With Zustand 5, how do you create a store, read it from a component, and update it without wrapping the app in a Provider?
answer
- one call returns a hook
- state and actions in one object
- set and get handed to the creator
- the selector decides what you get back
- store lives in module scope
basics
~20 sZustand's create((set, get) => ({...})) returns a hook bound to a module-level store. Components call that hook with a selector to read a slice, and actions defined inside the store call set to update it, so no Provider is needed.
solid answer
~40 sYou call `create` from `zustand` with a creator function `(set, get) => ({ ...state, ...actions })`. It builds the store once, at module load, and returns a hook such as `useCartStore`. A component reads with a selector, `useCartStore((s) => s.items.length)`, and re-renders only when that selected value changes (compared with `Object.is`). Actions like `addItem` live in the same object and call `set` with a partial state or an updater function; `set` merges it into the current state and notifies subscribers. There is no Provider because the store is an ordinary JavaScript object captured in a closure, not a React context value. The returned hook also carries the store API (`getState`, `setState`, `subscribe`, `getInitialState`), so non-React code can use the same store.
code
ts · 16 linesimport { create } from 'zustand'
type CartItem = { id: string; qty: number }
type CartState = {
items: CartItem[]
addItem: (id: string) => void
removeItem: (id: string) => void
}
export const useCartStore = create<CartState>()((set) => ({
items: [],
addItem: (id) =>
set((state) => ({ items: [...state.items, { id, qty: 1 }] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
}))go deeper
Recall the three moves: create with a creator that returns state plus actions, read with a selector passed to the hook, and update by calling set inside an action. No Provider is involved.
Explain why a selector narrows re-renders: the hook re-runs it after each change and compares results with Object.is. Show that set merges only one level deep.
Discuss the module-singleton trade-off: convenient in a browser app, but a shared instance across server requests or tests, which is when createStore plus useStore passed through Context is the answer.
Frame when the lack of structure matters: with no enforced action layer, conventions about where updates live and how stores are split have to be agreed and reviewed by the team.
## What `create` actually builds Zustand separates a **store** (a plain object that holds state and a set of listeners) from the **hook** React components use to read it. In Zustand 5 the entry point most apps use is `create`, imported from `zustand`. You pass it a **creator function** that receives three arguments — `set`, `get` and the store API — and returns the initial state object. Zustand runs the creator once, keeps the result as the current state, and returns a hook bound to that single store. ```ts import { create } from 'zustand' type CartItem = { id: string; qty: number } type CartState = { items: CartItem[] addItem: (id: string) => void clear: () => void } export const useCartStore = create<CartState>()((set) => ({ items: [], addItem: (id) => set((state) => ({ items: [...state.items, { id, qty: 1 }] })), clear: () => set({ items: [] }), })) ``` The curried `create<CartState>()(...)` form is the TypeScript idiom the docs use so the state type can be written once while middleware types are still inferred. ## Reading state: the hook and its selector A component calls the hook with a **selector**, a function from the whole state to the piece it needs: - The header badge calls `useCartStore((s) => s.items.length)` and gets a number. - The checkout page calls `useCartStore((s) => s.items)` and gets the array. - An action is read the same way: `useCartStore((s) => s.addItem)`; functions defined once in the creator keep a stable identity. Under the hood the hook subscribes through React's `useSyncExternalStore`. After every store change it re-runs the selector and compares the new result with the previous one using `Object.is`. If they are equal the component does not re-render. That is why selecting a primitive, or a reference the store already holds, keeps renders narrow. Calling the hook with **no selector** returns the entire state object, and since every `set` that changes anything produces a new state object, that component re-renders on every change in the store. ## Writing state: actions call `set` Zustand's core API has no separate action or reducer layer (an optional `redux` middleware exists for teams that want one). The idiomatic place for update logic is a function inside the store that calls `set`: 1. `set({ items: [] })` — pass a **partial** object; Zustand shallow-merges it into the current state. 2. `set((state) => ({ ... }))` — pass an **updater**; it receives the current state, which matters whenever the next value depends on the previous one. 3. `get()` — read the current state inside an action without subscribing, for example to validate before writing. The merge is one level deep: top-level keys you omit are kept, but a nested object you pass replaces the old nested object entirely. After merging, Zustand calls every listener synchronously with the new and previous state. ## Why there is no Provider The store is created when the module that calls `create` is first imported, and every component that imports the hook closes over the same instance. React never has to carry the store through the tree, so there is no Provider to mount and no context lookup at read time. The trade-off is that the store is a **module singleton**: one instance per JavaScript realm. When you need several independent instances — per server request, per test, per embedded widget — Zustand offers `createStore` from `zustand/vanilla` plus the `useStore` hook, and you pass the instance down yourself. ## The hook is also the store API `create` copies the store API onto the hook function, which gives code outside React the same store: | Member | What it does | |---|---| | `useCartStore.getState()` | returns the current state snapshot, no subscription | | `useCartStore.setState(partial)` | the same merge semantics as `set` | | `useCartStore.subscribe(listener)` | calls `listener(state, prevState)` on every change; returns an unsubscribe function | | `useCartStore.getInitialState()` | returns the state the creator first produced | This is what lets a plain module — a fetch wrapper, a WebSocket handler, a test — read or update the cart without rendering anything. ## Common mistakes - **Mutating state in place** (`get().items.push(item)`): no listener fires and the reference does not change, so selectors never see it. - **Destructuring the whole store** (`const { items } = useCartStore()`): correct output, but the component now re-renders on every unrelated change. - **Returning a fresh object or array from a selector** in v5: the new reference fails `Object.is` on every check; use `useShallow` or separate selectors.
- How would a plain TypeScript module, such as an API client, read or clear the Zustand cart store?The hook returned by `create` also carries the store API. The module imports `useCartStore` and calls `useCartStore.getState().items` to read, or `useCartStore.getState().clear()` / `useCartStore.setState({ items: [] })` to write. No hook rules apply because it is not calling the hook, only methods attached to it. Subscribed components still re-render because `setState` notifies listeners.
- What changes if the badge calls useCartStore() with no selector and destructures items from the result?The hook then returns the whole state object. Every `set` that changes anything produces a new state object, so the badge re-renders on every store update, including ones unrelated to the cart items. Output is still correct; only render scope widens. Selecting `s.items.length` keeps the badge silent until the count changes.
- Why do Zustand's docs place actions inside the store rather than in components?Actions defined in the creator are created once, keep a stable identity, and close over `set` and `get`, so any component or plain module can call them without re-implementing update logic. Selecting an action does not cause re-renders, because its reference never changes. Zustand also supports module-level functions that call `useCartStore.setState` if a team prefers that style.
saying these in an interview costs you the question
- You must wrap the app in a Zustand Provider before any component can read the store.
- Calling the store hook without a selector only re-renders when the fields you use change.
- Pushing into get().items is fine because Zustand notices the array was mutated.
- Every update needs an action type string and a separate reducer function.
- set replaces the whole state, so each action must spread every other field.