Skip to content
D
Documentation

Initialize scoped stores

how-to
3 min readUpdated

Use a vanilla store with React context when you need dependency injection or initial state from component props. Each provider supplies an independent store; consumers beneath that provider share it.

The store exists independently of React: createStore returns the store API, and useStore connects a React component to it. Context carries the store instance, not a bound hook.

The example below renders two bear counters initialized to 2 and 5. Clicking Add bear changes only the counter in that provider's scope. Run it in a browser React TypeScript project with Zustand installed:

bash
npm install zustand

1. Create a store factory

Accept initial props in a factory rather than creating one global instance. Each call returns a new StoreApi with its own state and listeners. Merge the initial props over your application defaults, then define the actions.

ts
import { createStore, type StoreApi } from 'zustand'

export type BearProps = {
  bears: number
}

export type BearState = BearProps & {
  addBear: () => void
}

export function createBearStore(
  initProps: Partial<BearProps> = {},
): StoreApi<BearState> {
  const defaults: BearProps = { bears: 0 }

  return createStore<BearState>()((set) => ({
    ...defaults,
    ...initProps,
    addBear: () => set((state) => ({ bears: state.bears + 1 })),
  }))
}

The functional update reads the previous count and returns the next count. The merging update preserves addBear in the store.

2. Provide a stable instance and a typed hook

Keep the store in a lazy useState initializer, following the provider-wrapper pattern. The provider retains that instance across re-renders instead of creating a new store on every render.

The consumer hook accepts a selector and returns its inferred result type. It reads the context, rejects a missing provider, and delegates the subscription to useStore.

tsx
import {
  createContext,
  useContext,
  useState,
  type PropsWithChildren,
} from 'react'
import { useStore, type StoreApi } from 'zustand'
import { createBearStore, type BearProps, type BearState } from './bear-store'

const BearContext = createContext<StoreApi<BearState> | null>(null)

type BearProviderProps = PropsWithChildren<Partial<BearProps>>

export function BearProvider({ children, bears }: BearProviderProps) {
  const [store] = useState(() => createBearStore({ bears: bears ?? 0 }))

  return (
    <BearContext.Provider value={store}>
      {children}
    </BearContext.Provider>
  )
}

export function useBearContext<T>(selector: (state: BearState) => T): T {
  const store = useContext(BearContext)

  if (store === null) {
    throw new Error('Missing BearProvider in the tree')
  }

  return useStore(store, selector)
}

function BearCount() {
  const bears = useBearContext((state) => state.bears)
  const addBear = useBearContext((state) => state.addBear)

  return (
    <section>
      <p>{bears} Bears.</p>
      <button type="button" onClick={addBear}>Add bear</button>
    </section>
  )
}

export default function BearScopeExample() {
  return (
    <main style={{ display: 'flex', gap: 32, minHeight: 160 }}>
      <BearProvider bears={2}><BearCount /></BearProvider>
      <BearProvider bears={5}><BearCount /></BearProvider>
    </main>
  )
}
The two independent scopes start with 2 Bears and 5 Bears, each with its own Add bear button.

3. Render independent scopes

Select the count and action separately. Both consumers use the same hook, but each reads the store supplied by its nearest provider.

tsx
import { BearProvider, useBearContext } from './bear-context'

function BearCounter({ label }: { label: string }) {
  const bears = useBearContext((state) => state.bears)
  const addBear = useBearContext((state) => state.addBear)

  return (
    <section>
      <h2>{label}</h2>
      <p>{bears} Bears.</p>
      <button type="button" onClick={addBear}>Add bear</button>
    </section>
  )
}

export default function App() {
  return (
    <main style={{ display: 'flex', gap: 32, minHeight: 160 }}>
      <BearProvider bears={2}>
        <BearCounter label="First scope" />
      </BearProvider>
      <BearProvider bears={5}>
        <BearCounter label="Second scope" />
      </BearProvider>
    </main>
  )
}

Mount App in your project's root element. This entry point expects an element with id="root" in your HTML:

tsx
import { createRoot } from 'react-dom/client'
import App from './App'

const container = document.getElementById('root')
if (container === null) {
  throw new Error('Missing root element')
}

createRoot(container).render(<App />)

You see First scope with 2 Bears. and Second scope with 5 Bears. Click the first scope's button: its count becomes 3, while the second stays at 5. Put additional consumers inside either provider to share that provider's store.

Inputs that matter

useStore takes the store instance and an optional selector; it has no options object.

OptionTypeDefaultWhat it does
Store argumentStoreApi<BearState> in this exampleRequiredChooses the instance to subscribe to.
Selector argument(state: BearState) => T in the consumer hookIdentity selector in useStore; required by useBearContextReturns the value the component reads.

The example's application-defined bears prop supplies the initial count and defaults to 0 when omitted. It is not a Zustand configuration option.

Pitfalls

  • Treat provider props as initialization, not synchronization. The lazy initializer captures the initial props; later prop changes do not update the retained store. Use a store action for subsequent updates.
  • Keep each consumer under BearProvider. The custom hook throws when the context contains null.
  • Do not create the store directly in the provider's render body. Keep the lazy initializer so re-renders retain the current count.
  • For selectors that derive objects or arrays, see Selectors and subscriptions.
  • For server rendering and request-local initialization, see Next.js store setup.

Live demo

Try the Zustand live demo to see a counter driven by a store action. The scoped example above adds context and a store factory so separate counters do not share one global store.

Was this page helpful?

Initialize scoped stores — zustand · GPT-6.1 Sol