effect global state pattern
a pattern for managing reactive app state using Effect's SubscriptionRef and Atom runtime — accessible from both React components and non-React contexts (interceptors, WebSockets, utilities) without duplication or manual sync.
this pattern is based on Harry Solovay's tweet: https://x.com/harrysolovay/status/2054956734284734716
core concept
traditional React state (useState, Zustand, Jotai) is React-only. if you need the same state in an Axios interceptor or a WebSocket handler, you end up maintaining a separate store and syncing them manually.
this pattern solves that by placing state inside an Effect service layer, then exposing it through an Atom Runtime that works in any context.
architecture
┌────────────────────────────────────────────────────┐
│ Effect Layer │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ SubscriptionRef<State> │ │
│ │ (single source of truth) │ │
│ └──────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼──────────────────────────┐ │
│ │ AppState Service │ │
│ │ Context.Service<AppState> │ │
│ └──────────────────┬──────────────────────────┘ │
└─────────────────────-│─────────────────────────────┘
│ wrapped by
▼
┌───────────────────────┐
│ Atom Runtime │
│ runtime.Provider │
└────────┬──────────────┘
│
┌─────────┴──────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────────────┐
│ Inside │ │ Outside React │
│ React │ │ │
│ │ │ Axios interceptors │
│ useAtom() │ │ WebSocket handlers │
│ │ │ Utility functions │
└──────┬──────┘ └──────────┬──────────┘
│ │
▼ ▼
auto re-render runtime.get()
on state change .subscribe()
key pieces
| piece | role |
|---|---|
SubscriptionRef |
holds state, broadcasts changes to all subscribers |
AppState service |
registers the ref as an injectable Effect service |
Atom Runtime |
bridges the Effect layer to the outside world |
rootStateAtom |
the public handle, read/write from anywhere |
countAtom |
derived atom, plucks a slice, avoids unnecessary re-renders |
implementation
1. state definition — state.ts
import { Context, Layer } from "effect"
import { SubscriptionRef } from "effect"
import { Atom } from "@effect-rx/rx"
// shape of your app state
type State = { count: number }
const makeInitial = (): State => ({ count: 0 })
// effect service wrapping a reactive SubscriptionRef
export class AppState extends Context.Service<AppState>()("AppState", {
make: SubscriptionRef.make(makeInitial()),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// atom runtime — the bridge between Effect and the outside world
export const runtime = Atom.runtime(AppState.layer)
// rootStateAtom — single source of truth, usable anywhere
export const rootStateAtom = runtime.subscriptionRef(AppState.asEffect())
// derived atom — only triggers re-render when count specifically changes
export const countAtom = rootStateAtom.pipe(
Atom.mapResult(({ count }) => count)
)
2. app entry point — main.tsx
wrap your app with runtime.Provider so all child components can access atoms.
import { runtime } from "./state"
function App() {
return (
<runtime.Provider>
<Counter />
<OtherComponents />
</runtime.Provider>
)
}
3. inside React — Counter.tsx
use the useAtom hook. the component re-renders automatically when the atom value changes.
import { countAtom, rootStateAtom } from "./state"
function Counter() {
// subscribes to countAtom — re-renders only when count changes
const count = useAtom(countAtom)
const increment = () =>
rootStateAtom.update(state => ({ ...state, count: state.count + 1 }))
const decrement = () =>
rootStateAtom.update(state => ({ ...state, count: state.count - 1 }))
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
4. Outside React
two modes depending on whether you need a snapshot or a live subscription.
snapshot - Axios interceptor
import axios from "axios"
import { runtime, rootStateAtom } from "./state"
// runtime.get() — synchronous one-time read
axios.interceptors.request.use((config) => {
const state = runtime.get(rootStateAtom)
config.headers["X-Count"] = state.count
return config
})
reactive - subscribe to changes
import { rootStateAtom } from "./state"
// .subscribe() — fires a callback on every state change
const unsubscribe = rootStateAtom.subscribe((state) => {
console.log("state changed:", state.count)
analytics.track("count_updated", { value: state.count })
})
// clean up when no longer needed
unsubscribe()
consumption patterns
rootStateAtom
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
useAtom() runtime.get() .subscribe()
│ │ │
React hook Sync snapshot Reactive cb
│ │ │
auto re-render read once on fires on
when state demand (e.g. every change
changes interceptor) (e.g. logger)
| method | context | behaviour |
|---|---|---|
useAtom(atom) |
React components | reactive, triggers re-renders |
runtime.get(atom) |
anywhere | synchronous snapshot, one-time read |
atom.subscribe(cb) |
anywhere | reactive, fires callback on every change |
derived atoms
derived atoms let you subscribe to a slice of state instead of the whole object, preventing unnecessary re-renders.
// full state atom, re-renders on any state change
const rootStateAtom = runtime.subscriptionRef(AppState.asEffect())
// derived, only re-renders when count changes
const countAtom = rootStateAtom.pipe(
Atom.mapResult(({ count }) => count)
)
// another derived, only re-renders when user changes
const userAtom = rootStateAtom.pipe(
Atom.mapResult(({ user }) => user)
)
rootStateAtom { count, user, theme, ... }
│
├──── countAtom ──► CounterComponent
├──── userAtom ──► UserProfileComponent
└──── themeAtom ──► ThemeToggleComponent
each component only re-renders when its specific slice changes.
summary
state lives in Effect (SubscriptionRef)
↓
wrapped in AppState service
↓
exposed via Atom Runtime
↓
consumed as atoms — same API everywhere
React? → useAtom(atom)
Interceptor? → runtime.get(atom)
Side effect? → atom.subscribe(cb)
the key insight: state is defined once, in one place, and consumed uniformly regardless of where in the app you are. no duplicate stores, no manual sync, no context passing.
dependencies
effect — core Effect library
@effect-atom/atom — Atom runtime
@effect-atom/atom-react — useAtom hook for React