Skip to content
D
Documentation

Store scope and lifecycle

concept
4 min readUpdated

Store scope is the boundary you choose for sharing a store instance; its lifetime depends on where your application keeps that instance.

The store exists independently of React, and hooks connect React components to it. Creating a store, subscribing a component, and hydrating persisted state are separate operations: mounting a consumer does not create a new instance of an existing store.

Ownership determines sharing

create creates a store and returns a hook bound to it. Every component calling that same hook reads the same store. A module-level hook therefore gives the components importing it shared state without a provider.

createStore returns a vanilla store instead of a bound hook. Call it inside a factory when you need independent instances. For dependency injection or initialization from component props, keep a vanilla store in React context. useStore connects a consumer to the instance supplied by that context and returns its selected state.

mermaid
flowchart TD
  M["Module-level create()"] --> G["Shared store and bound hook"]
  G --> A["Consumer A"]
  G --> B["Consumer B"]
  P["Provider with lazy useState initializer"] --> V["Vanilla store instance"]
  V --> C["Context supplies the instance"]
  C --> D["useStore(instance, selector)"]
  D --> E["Scoped consumer"]
  S["Persisted storage"] -->|"Hydration merges saved state"| V

Choose the owner according to the sharing you want:

  • Module: components using the same exported hook or vanilla store share one instance. Unmounting a consumer does not remove the module's reference to that store.
  • Provider: descendants share the provider's instance. Separate providers can own separate instances, even when they use the same factory.
  • Keyed collection: a map can retain one instance per tab or other key. Switching consumers does not remove the entries; the collection owns those references.

The authors' dynamic-store example uses a map to reuse each tab's store. A component subscription is not an ownership mechanism: removing the subscription does not reset the state.

A module-level store shares updates

In a browser React project with a root element, use this as src/main.tsx. Both buttons start at zero. Clicking either button increments the count displayed by both, because both consumers use the same bound hook.

tsx
import { createRoot } from 'react-dom/client'
import { create } from 'zustand'

type CounterState = {
  count: number
  increment: () => void
}

const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter({ label }: { label: string }) {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  return (
    <button type="button" onClick={increment}>
      {label}: {count}
    </button>
  )
}

function App() {
  return (
    <main>
      <Counter label="First consumer" />
      <Counter label="Second consumer" />
    </main>
  )
}

createRoot(document.getElementById('root')!).render(<App />)

This follows the starter's module-level store and separate selectors for state and actions. The live Zustand demo also uses a module-level bound hook for its counter.

A provider initializes an independent instance

Use this alternative src/main.tsx to give each provider its own counter. The first provider starts at two and has two consumers; the second starts at ten and has one. Clicking either button in the first group updates both of that group's buttons, but leaves the second group unchanged.

The lazy useState initializer keeps the instance stable across provider re-renders. initialCount supplies creation-time data, not an ongoing binding: changing that prop on the same mounted provider does not recreate or update its store. A new provider mount creates a new instance from its initial props.

tsx
import { createContext, useContext, useState, type ReactNode } from 'react'
import { createRoot } from 'react-dom/client'
import { createStore, useStore } from 'zustand'

type CounterState = {
  count: number
  increment: () => void
}

function createCounterStore(initialCount: number) {
  return createStore<CounterState>()((set) => ({
    count: initialCount,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
}

type CounterStore = ReturnType<typeof createCounterStore>
const CounterContext = createContext<CounterStore | null>(null)

function CounterProvider({
  initialCount,
  children,
}: {
  initialCount: number
  children: ReactNode
}) {
  const [store] = useState(() => createCounterStore(initialCount))
  return (
    <CounterContext.Provider value={store}>
      {children}
    </CounterContext.Provider>
  )
}

function useCounter<T>(selector: (state: CounterState) => T): T {
  const store = useContext(CounterContext)
  if (!store) throw new Error('Missing CounterProvider')
  return useStore(store, selector)
}

function Counter({ label }: { label: string }) {
  const count = useCounter((state) => state.count)
  const increment = useCounter((state) => state.increment)
  return (
    <button type="button" onClick={increment}>
      {label}: {count}
    </button>
  )
}

function App() {
  return (
    <main>
      <CounterProvider initialCount={2}>
        <section>
          <h2>First scope</h2>
          <Counter label="Consumer A" />
          <Counter label="Consumer B" />
        </section>
      </CounterProvider>
      <CounterProvider initialCount={10}>
        <section>
          <h2>Second scope</h2>
          <Counter label="Consumer C" />
        </section>
      </CounterProvider>
    </main>
  )
}

createRoot(document.getElementById('root')!).render(<App />)

Context carries the vanilla instance, not a bound hook. For a reusable provider pattern, continue with Initialize scoped stores.

Initialization and persistence hydration are different

The state creator runs when you create the store and returns its initial state and actions. A later component subscription reads that existing store; it does not run the state creator again. For construction-time constraints, see Reset store state.

persist adds another stage: hydration retrieves persisted state from storage and merges it with current state. It does not change which components share the instance.

  • With synchronous storage such as localStorage, normal automatic hydration completes during store creation.
  • With asynchronous storage, hydration completes later, in a microtask. The initial React render can precede that completion. See Persist store data for handling that timing.
  • With skipHydration: true, automatic hydration at initialization is disabled. Call the instance's persist.rehydrate() when your application is ready; it returns void or a promise. See Control persistence hydration.

In-memory ownership and storage identity are separate. The persistence name identifies the storage entry and must be unique. Separate scoped instances using the same name read and write that same entry, even though their in-memory state is independent.

Subscriptions connect React without owning the store

The state and listeners behind StoreApi belong to the store instance, not its consumers; see State and actions for the individual methods.

useStore uses React's useSyncExternalStore with the instance's subscription and snapshot methods. React manages that component's subscription lifecycle. Unmounting a consumer ends its subscription; it does not reset or delete the store. Select only the state each consumer needs, as in the samples above. See Selectors and subscriptions for selector-output equality and rendering behavior.

When you register a listener yourself with subscribe(), retain its unsubscribe function and call it when that listener's owner tears down. That removes the listener, not the state. See Subscribe outside React for an effect with cleanup.

React's server snapshot uses getInitialState(). With persistence, that method returns the state creator's result, while hydration can change current state. Persistence hydration is not React's server/client hydration. For request ownership and matching server/client initialization, follow Next.js store setup.

Was this page helpful?