# Subscribe outside React

Use a selected subscription to update a non-React consumer only when its chosen value changes; see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) for creating a store with [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and reading or updating its state.

This browser example shows the same counter in a React heading and an external consumer's output. Clicking **Increment outside React** updates both. Clicking **Toggle snout** changes another field without notifying the counter's selected subscription.

## 1. Enable selected subscriptions

In your React TypeScript project, install Zustand:

```bash
npm install zustand
```

Wrap the state creator with [`subscribeWithSelector`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#subscribewithselector). This shipped middleware adds `subscribe(selector, listener, options)` while preserving the whole-store subscription signature.

```ts title="store.ts"
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

type CounterState = {
  count: number
  snout: boolean
  toggleSnout: () => void
}

export const useCounterStore = create<CounterState>()(
  subscribeWithSelector((set) => ({
    count: 0,
    snout: true,
    toggleSnout: () => set((state) => ({ snout: !state.snout })),
  })),
)
```

The result is a React hook with store methods attached. Keep the inferred return type so the middleware's selected `subscribe()` overload remains available.

## 2. Read, update and subscribe without calling a hook

`getState()` returns the current state without subscribing. Use `setState()` for an external update; the functional form reads the current count, and the default shallow merge preserves the other fields and action.

The listener receives the new and previous selected values. `fireImmediately: true` also invokes it during setup, with the current value as both arguments. Return the unsubscribe function to whoever owns the consumer's lifetime.

```ts title="counter-consumer.ts"
import { useCounterStore } from './store'

export function incrementOutsideReact(): void {
  useCounterStore.setState((state) => ({ count: state.count + 1 }))
}

export function attachCounterOutput(output: HTMLOutputElement): () => void {
  output.textContent = `Count: ${useCounterStore.getState().count}`

  return useCounterStore.subscribe(
    (state) => state.count,
    (count, previousCount) => {
      output.textContent = `Count: ${count} (previous: ${previousCount})`
    },
    { fireImmediately: true },
  )
}
```

This module runs in the browser and never calls the React hook. The selected listener runs synchronously when the count changes; changing only `snout` leaves its output untouched.

For all state changes rather than one selected value, pass only a listener to `subscribe()`. That listener receives the complete current state and previous state, and the call still returns an unsubscribe function.

## 3. Mount the consumer and clean up on unmount

Give the external consumer its own output element. React selects `count` and `snout` through the hook, while the effect connects the external consumer after the element mounts. Returning its unsubscribe function from the effect disconnects that listener on unmount.

```tsx title="App.tsx"
import { useEffect, useRef } from 'react'
import { attachCounterOutput, incrementOutsideReact } from './counter-consumer'
import { useCounterStore } from './store'

export default function App() {
  const count = useCounterStore((state) => state.count)
  const snout = useCounterStore((state) => state.snout)
  const toggleSnout = useCounterStore((state) => state.toggleSnout)
  const outputRef = useRef<HTMLOutputElement>(null)

  useEffect(() => {
    const output = outputRef.current
    if (!output) return
    return attachCounterOutput(output)
  }, [])

  return (
    <section>
      <h1>React count: {count}</h1>
      <p>External consumer: <output ref={outputRef} /></p>
      <p>Snout: {snout ? 'on' : 'off'}</p>
      <button onClick={incrementOutsideReact}>Increment outside React</button>
      <button onClick={toggleSnout}>Toggle snout</button>
    </section>
  )
}
```

Mount it in your project's entry point, with a `<div id="root"></div>` in the HTML:

```tsx title="main.tsx"
import { createRoot } from 'react-dom/client'
import App from './App'

const container = document.getElementById('root')!
createRoot(container).render(<App />)
```

The heading starts at `React count: 0`, and the external output starts at `Count: 0 (previous: 0)`. Increment once to see the heading change to `1` and the output to `Count: 1 (previous: 0)`. Toggle snout to see its text change while the counter output stays the same.

## Subscription options

Pass these options as the third argument of the selected `subscribe()` overload:

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `equalityFn` | `(a: U, b: U) => boolean` | `Object.is` | Compares selected values. The listener runs only when the comparison returns `false`. `U` is the selector's result type. |
| `fireImmediately` | `boolean` | No immediate call | Invokes the listener during setup with the current selected value as both arguments. |

## Pitfalls

- Without `fireImmediately`, subscribing registers a listener but does not render the initial value. Read with `getState()` and initialize the consumer yourself, or enable the option.
- Unsubscribe when the consumer tears down, not immediately after subscribing. Unsubscribing removes that listener; it does not remove the store.
- Middleware that modifies the state creator's `set` or `get` does not necessarily modify the public `setState()` or `getState()` methods. Use your store's actions when you need their update behavior.
- For immutable updates and nested merging, see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions). For server-side store ownership, see [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup).

## Live demo and related guides

Explore the authors' [live Zustand demo](https://zustand-demo.pmnd.rs/) for the counter hook in action. Its [source](https://github.com/pmndrs/zustand/blob/main/examples/demo/src/App.jsx) shows the React counter; the example above adds an external selected subscription.

Apply the same external subscription and cleanup pattern to a store created with [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore); see [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works) for standalone stores and connecting them to React with [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore).

As an alternative entry point, this sample mounts a React counter connected to a vanilla store. Clicking the button updates the store through `setState()` and the selected count appears in the heading.

```tsx title="main.tsx"
import { createRoot } from 'react-dom/client'
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'

type CounterState = { count: number }

const counterStore = createStore<CounterState>()(() => ({ count: 0 }))

function incrementOutsideReact(): void {
  counterStore.setState((state) => ({ count: state.count + 1 }))
}

function VanillaCounter() {
  const count = useStore(counterStore, (state) => state.count)
  return (
    <section>
      <h1>React count: {count}</h1>
      <button onClick={incrementOutsideReact}>Increment outside React</button>
    </section>
  )
}

const container = document.getElementById('root')!
createRoot(container).render(<VanillaCounter />)
```

![The React heading displays the vanilla store's initial count of 0 above the external increment button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/b32011c738aa03452afeb766a744ec2a.png)

- [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions)
- [Store scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle)
