| | |

Zustand 01 ⚛️ Installation — Store setup & provider-free boilerplate

⏱️ Estimated read: 13 min | |

Zustand is a state management library built on a single premise: state should be accessible without ceremony. Where Redux requires a provider wrapping the application, reducers, and action creators, and React Context requires a provider and careful memoization to avoid re-rendering every consumer, Zustand requires one function call. You install the package, call create, and export a hook. Any component that imports the hook can read from the store and subscribe to changes. There is no provider, no boilerplate, and no wrapper component .

This chapter covers the installation, the creation of a store, the TypeScript-specific syntax that v5 requires, and the mechanism that makes the store available to components without a provider. It is the foundation for every subsequent Zustand chapter: selectors, middleware, async actions, and persistence all build on the store created here.

Key point: Zustand stores are hooks. create returns a custom hook that components call with a selector. The hook reads from a module-scoped store that exists independently of the React tree, which is why no provider is needed. In TypeScript, the v5 syntax is create<T>()(...) with double parentheses, a curried form that allows type inference to work correctly with middleware.


Why Zustand eliminates the provider

The provider problem.

React Context requires a provider component to be placed at or above the components that consume the context. This provider creates a boundary: components below it can access the value, components above it cannot. In a large application, providers stack—theme, authentication, feature flags, state—and each one adds a layer of nesting and a dependency on the tree structure. Zustand avoids this entirely because the store is not distributed through React context. It is a module-level object. The hook returned by create closes over that object and subscribes to it directly, using React’s useSyncExternalStore internally . The component tree is irrelevant to the store’s accessibility.

The boilerplate problem.

Redux’s architecture separates state, actions, reducers, and the store into distinct files and concepts. This separation is valuable for large teams and complex update logic, but it is overhead for applications that need a single shared counter or a user session object. Zustand collapses the separation: the store contains both state and the functions that modify it. The set function merges partial updates into the existing state, so the developer writes only what changes . There is no action type, no switch statement, and no dispatch.

The TypeScript inference problem.

In Zustand v5, the create function requires a curried syntax for TypeScript: create<State>()(...) with an empty pair of parentheses before the state initializer . This is not stylistic. Because set and get both take and return the same type, TypeScript cannot infer the state type from the initializer alone. The curried form allows the developer to supply the type explicitly while the middleware types are still inferred correctly . Writing create<State>(...) without the extra () produces type errors in v5, particularly when middleware is added later.


a. Installing the package

Zustand is published as a single package with no peer dependencies beyond React. It works with React 18 and later, including React 19 .

# Install with npm
npm install zustand

# Or with pnpm
pnpm add zustand

# Or with yarn
yarn add zustand

# Or with bun
bun add zustand

The package includes the core create function and all middleware (persist, devtools, immer) within the same installation. Optional middleware like Immer requires a separate install of the immer package, but the Zustand middleware itself does not . The base installation is complete once the package is in node_modules and the import resolves.


b. Creating the store with create

The create function accepts a state initializer and returns a hook. The initializer receives set and get as arguments. The set function updates the store; the get function reads the current state .

// src/stores/counter.ts
import { create } from 'zustand'

// Define the shape of the store: state values and actions
interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

// create<T>()() is the v5 TypeScript syntax
// The double parentheses are required for correct type inference
export const useCounterStore = create<CounterState>()((set) => ({
  // Initial state
  count: 0,

  // Actions modify state through set
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

The set function merges the returned object with the current state. Only the fields that are returned are changed; everything else remains untouched . This is why reset: () => set({ count: 0 }) does not affect the actions—they are merged in, not replaced. The second argument to set can be true to replace the entire state, but this is rarely used because it removes the actions as well.

The get function reads the current state without subscribing. It is useful for actions that need to read a value before updating it.

// Using get to read state inside an action
interface ProfileState {
  firstName: string
  lastName: string
  fullName: () => string
  setName: (first: string, last: string) => void
}

export const useProfileStore = create<ProfileState>()((set, get) => ({
  firstName: '',
  lastName: '',
  fullName: () => `${get().firstName} ${get().lastName}`,
  setName: (first, last) => set({ firstName: first, lastName: last }),
}))

c. Using the store in components

The hook returned by create is called with a selector function. The selector extracts the specific slice of state the component needs, and the component re-renders only when that slice changes .

// src/components/Counter.tsx
import { useCounterStore } from '../stores/counter'

function CounterDisplay() {
  // Select only the count value
  // This component re-renders when count changes, and only then
  const count = useCounterStore((state) => state.count)

  return <p>Count: {count}</p>
}

function CounterControls() {
  // Select the actions individually
  // Actions never change, so this component never re-renders after mount
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)
  const reset = useCounterStore((state) => state.reset)

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

The selector is not optional in practice. Calling useCounterStore() without a selector returns the entire store and subscribes the component to every change. This defeats the purpose of fine-grained subscriptions and causes unnecessary re-renders. The selector should return the smallest value the component needs.


Complete Example Session

// ============================================
// PART 1: THE STORE DEFINITION
// ============================================
// A todo store demonstrates state, actions, and TypeScript types
import { create } from 'zustand'

interface Todo {
  id: number
  text: string
  completed: boolean
}

interface TodoState {
  todos: Todo[]
  filter: 'all' | 'active' | 'completed'
  addTodo: (text: string) => void
  toggleTodo: (id: number) => void
  setFilter: (filter: TodoState['filter']) => void
  clearCompleted: () => void
}

// ============================================
// PART 2: CREATING THE STORE
// ============================================
// create<T>()() with double parentheses for v5 TypeScript
export const useTodoStore = create<TodoState>()((set) => ({
  // Initial state
  todos: [],
  filter: 'all',

// ============================================
// PART 3: THE ADD ACTION
// ============================================
  // set merges the new todos array into state
  // The spread operator creates a new array; no mutation occurs
  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, completed: false }],
    })),

// ============================================
// PART 4: THE TOGGLE ACTION
// ============================================
  // map creates a new array; the toggled todo is a new object
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),

// ============================================
// PART 5: THE FILTER ACTION
// ============================================
  // A simple value update; set merges it into state
  setFilter: (filter) => set({ filter }),

// ============================================
// PART 6: THE CLEAR ACTION
// ============================================
  // filter removes completed todos, creating a new array
  clearCompleted: () =>
    set((state) => ({
      todos: state.todos.filter((todo) => !todo.completed),
    })),
}))

// ============================================
// PART 7: A COMPONENT WITH SELECTORS
// ============================================
import { useTodoStore } from './todo-store'

function TodoList() {
  // Select only the todos array
  const todos = useTodoStore((state) => state.todos)

  // Select only the filter value
  const filter = useTodoStore((state) => state.filter)

  // Select only the clearCompleted action
  const clearCompleted = useTodoStore((state) => state.clearCompleted)

  // Derive the filtered list without storing it
  const filtered = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed
    if (filter === 'completed') return todo.completed
    return true
  })

  return (
    <div>
      <ul>
        {filtered.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <button onClick={clearCompleted}>Clear completed</button>
    </div>
  )
}

The seven parts demonstrate the complete pattern: define the interface, create the store with actions, and consume it with selectors. The store is a module-level object; no provider wraps TodoList, and no context distributes the store. The component imports the hook and calls it.


Quick Reference

Installation Commands

PackageCommandNote
Corenpm install zustandIncludes all middleware
Immer (optional)npm install immerRequired only for immer middleware

Store Creation

StepCodeLocation
Importimport { create } from 'zustand'Store file
Define interfaceinterface State { ... }Store file
Create storecreate<State>()((set) => ({ ... }))Module scope
Export hookexport const useStore = ...Store file

v5 TypeScript Syntax

VersionSyntaxStatus
v4create<State>((set) => ...)Deprecated
v5create<State>()((set) => ...)Required

Best Practices

✅ Do This:

// Use the curried syntax in TypeScript
create<State>()((set) => ({ ... }))                         // ✅

// Define a separate interface for the store
interface Store { count: number; increment: () => void }    // ✅

// Use selectors to extract specific state
const count = useStore((state) => state.count)              // ✅

// Use set with an updater function for derived state
set((state) => ({ count: state.count + 1 }))                // ✅

// Keep actions inside the store, not in components
increment: () => set((state) => ({ count: state.count + 1 })) // ✅

// Export the hook from the store module
export const useStore = create<State>()(...)                // ✅

❌ Don’t Do This:

// Use v4 syntax in v5
create<State>((set) => ({ ... }))                           // ❌ Type errors

// Call the hook without a selector
const store = useStore()    // Subscribes to everything         // ❌

// Mutate state directly
state.count++    // No re-render; subscribers not notified    // ❌

// Create the store inside a component
function Component() { const useStore = create(...) }        // ❌

// Store derived values that can be computed in selectors
todos: [], filteredTodos: []    // Redundant and stale          // ❌

// Use a provider when it is not needed
<StoreProvider>    // Zustand does not require this             // ❌

Common Pitfalls

PitfallWhy It HappensFix
Type errors on createv5 requires create<T>()()Add the extra ()
Component re-renders on every changeNo selector passed to the hookAlways use a selector
State changes but UI does not updateState mutated directlyUse set with new objects
Store recreated on every rendercreate called inside componentMove to module scope
get() returns stale valueget reads at call time, not reactivelyUse get inside actions for current state

Real-World Examples

1. Minimal JavaScript store

import { create } from 'zustand'
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

2. TypeScript store with interface

interface State { count: number; increment: () => void }
const useStore = create<State>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

3. Selector for a single value

const count = useStore((state) => state.count)

4. Selector for an action

const increment = useStore((state) => state.increment)

5. Using get inside an action

double: () => get().count * 2

6. Reset action

reset: () => set({ count: 0 })

7. Store with an array

items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] }))

8. Toggle action

toggle: () => set((state) => ({ isOpen: !state.isOpen }))

9. Async action

fetchData: async () => {
  const data = await fetch('/api').then(r => r.json())
  set({ data })
}

10. Multiple selectors in one component

const count = useStore((s) => s.count)
const increment = useStore((s) => s.increment)

Visual

Provider-Free Architecture

┌──────────────────────────────────────────────────────────────┐
│  ZUSTAND STORE EXISTS OUTSIDE THE REACT TREE                 │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ MODULE SCOPE (not React)                               │  │
│  │                                                        │  │
│  │ const useStore = create((set) => ({ ... }))            │  │
│  │       │                                                │  │
│  │       ▼                                                │  │
│  │ ┌──────────────────────────────────────────────────┐   │  │
│  │ │ Store Object                                      │   │  │
│  │ │ ├─ State: { count: 0 }                            │   │  │
│  │ │ ├─ Actions: { increment, decrement }              │   │  │
│  │ │ └─ Subscribers: Set<Component>                    │   │  │
│  │ └──────────────────────────────────────────────────┘   │  │
│  └────────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           │ Hook subscribes directly         │
│                           │ (no context, no provider)        │
│                           ▼                                  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ REACT COMPONENT TREE                                   │  │
│  │                                                        │  │
│  │  <App>                                                 │  │
│  │    <Header />                                          │  │
│  │      <Counter />    ← imports useStore, calls it       │  │
│  │    <Content />                                         │  │
│  │      <Display />    ← imports useStore, calls it       │  │
│  │                                                        │  │
│  │  No <Provider> wraps any of these.                     │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Compare: Redux/Context require a provider at the root.      │
│  Zustand requires only an import.                            │
└──────────────────────────────────────────────────────────────┘

The v5 TypeScript Syntax

┌──────────────────────────────────────────────────────────────┐
│  WHY create<T>()() HAS DOUBLE PARENTHESES                    │
│                                                              │
│  create<State>()((set) => ({ ... }))                         │
│         │        │                                           │
│         │        └── State initializer (function)            │
│         └── Curried type parameter                           │
│                                                              │
│  Without currying:                                           │
│  create<State>((set) => ({ ... }))                           │
│         │                                                    │
│         └── TypeScript cannot infer middleware types         │
│             when middleware is added later                   │
│                                                              │
│  With currying:                                              │
│  create<State>()(...)                                        │
│         │                                                    │
│         └── Type parameter is fixed first                    │
│             Middleware types are inferred second             │
│             Full type safety with persist, devtools, etc.    │
│                                                              │
│  v5 requires the curried form. v4 accepted both.             │
└──────────────────────────────────────────────────────────────┘

set Merges, Does Not Replace

┌──────────────────────────────────────────────────────────────┐
│  set({ count: 1 }) MERGES WITH EXISTING STATE                │
│                                                              │
│  Initial state:                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ { count: 0, name: 'Ada', increment: [Function] }       │  │
│  └────────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│  set({ count: 1 })                                           │
│                           │                                  │
│                           ▼                                  │
│  Resulting state:                                            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ { count: 1, name: 'Ada', increment: [Function] }       │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Only count changed. name and increment remain.              │
│  The merge is shallow, not deep.                             │
│                                                              │
│  To replace entirely: set({ count: 1 }, true)                │
│  → Removes name and increment. Rarely used.                  │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Packagezustand
React requirement18.0 or later
Create functionimport { create } from 'zustand'
v5 TypeScript syntaxcreate<T>()((set) => ...)
ProviderNot required
Store locationModule scope
Hook usageuseStore((state) => state.value)
set behaviorShallow merge by default
get behaviorReads current state at call time
Selector requirementRequired for fine-grained re-renders

Key takeaways:

  • Zustand requires no provider. The store is a module-level object. The hook subscribes to it directly, bypassing React context entirely. Components import the hook and call it—nothing wraps them .
  • The v5 TypeScript syntax is create<T>()(). The double parentheses are not optional. They allow type inference to work correctly with middleware. The v4 form without currying produces type errors in v5 .
  • Selectors are mandatory for performance. Calling the hook without a selector returns the entire store and subscribes the component to every change. useStore((state) => state.count) subscribes only to count .
  • set merges; it does not replace. Only the fields returned by the setter are updated. Actions and other state values remain untouched. The shallow merge is one level deep; nested objects must be copied manually .
  • The store contains both state and actions. There is no separation between reducers and action creators. The increment function lives alongside the count value in the same object.
  • get reads without subscribing. Inside an action, get() returns the current state. It is used for logic that depends on existing values, not for rendering.
  • Module scope is the correct location. A store created inside a component is recreated on every render, discarding all state. The store must be defined once at the module level.

Remember: Zustand’s installation is one command, and its setup is one function call. The absence of a provider is not a missing feature; it is the design. The store lives outside the React tree, and the hook connects components to it without wrapping them. Use the curried TypeScript syntax, always pass a selector, and treat set as a merge operation. The store you create in this chapter is the foundation for selectors, middleware, and persistence. Get the creation syntax right, and every subsequent pattern follows.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!