Skip to content
D
Documentation

Subscribe outside React

how-to
2 min readUpdated

Subscribe to a vanilla store outside React

Use a vanilla store when browser code outside React needs to read state, update it, or react to changes without rendering a React component. The page below shows two live values; clicking either button updates its value through a selected subscription.

Create the store and subscribe

  1. Install zustand:

    bash
    npm install zustand
    
  2. Put this markup in the page that loads your TypeScript entry point. It gives the example a mounted UI with two controls and two value displays:

    html
    <section>
      <p>X: <output id="x-value"></output></p>
      <button id="increment-x" type="button">Increment X</button>
      <p>Y: <output id="y-value"></output></p>
      <button id="increment-y" type="button">Increment Y</button>
    </section>
    
  3. In that entry point, create a store with createStore from zustand/vanilla. Wrap its state creator with subscribeWithSelector from zustand/middleware, then subscribe to each coordinate:

    ts
    import { subscribeWithSelector } from 'zustand/middleware'
    import { createStore } from 'zustand/vanilla'
    
    type Coordinates = { x: number; y: number }
    
    const store = createStore<Coordinates>()(
      subscribeWithSelector(() => ({ x: 0, y: 0 })),
    )
    
    const xValue = document.getElementById('x-value')!
    const yValue = document.getElementById('y-value')!
    const incrementXButton = document.getElementById('increment-x')!
    const incrementYButton = document.getElementById('increment-y')!
    
    const initialState = store.getState()
    xValue.textContent = String(initialState.x)
    yValue.textContent = String(initialState.y)
    
    store.subscribe(
      (state) => state.x,
      (x, previousX) => {
        xValue.textContent = `${x} (was ${previousX})`
      },
      { fireImmediately: true },
    )
    
    store.subscribe(
      (state) => state.y,
      (y, previousY) => {
        yValue.textContent = `${y} (was ${previousY})`
      },
      { fireImmediately: true },
    )
    
    incrementXButton.addEventListener('click', () => {
      store.setState((state) => ({ x: state.x + 1 }))
    })
    
    incrementYButton.addEventListener('click', () => {
      store.setState((state) => ({ y: state.y + 1 }))
    })
    
Both selected values appear as 0, with their initial previous values shown.

For the vanilla store's API surface, see React and vanilla stores. Here, subscription callbacks update ordinary DOM outputs directly, without a React component rendering the values.

Selector subscription options

The selector form accepts an optional third argument:

OptionTypeDefaultWhat it does
equalityFn(a: U, b: U) => booleanObject.isCompares the current and next selector results; the callback runs when they are not equal according to this function.
fireImmediatelybooleanfalseCalls the callback as soon as it subscribes, passing the current selected value as both the new and previous values.

Use the default comparison for scalar selections such as state.x. If you select a newly created object or array, provide an equality function appropriate to that value so equivalent selections do not trigger the callback.

Pitfalls

  • For the subscribe and middleware API details, see Use a vanilla store in React; here, each DOM output tracks only its own coordinate.
  • When middleware changes the set or get functions supplied to a state creator, those changes do not carry over to the vanilla store's setState and getState utilities. Use the store's actions when you need the middleware-wrapped behavior.
  • For the Next.js shared-store and React Server Component caveat, see Set up Zustand in Next.js.

Live demo

Try the Zustand live demo to see a running Zustand application.

Was this page helpful?