Vanilla TypeScript Core State Management Patterns
The foundational approach is a typed observable store — a generic Store<T>
class that holds state, exposes getState(), and notifies subscribers via
setState(). TypeScript generics enforce that only valid keys/shapes can be
set. Three main patterns build on this:
-
Observable Store — a central class with
subscribe/setState, good for simple-to-medium apps -
Reducer — Redux-style discriminated union actions processed by a pure
reducerfunction; TypeScript’s exhaustive checking ensures no action is missed -
Proxy-based reactivity — intercepts property assignments to auto-trigger re-renders, mimicking Vue/MobX without a library
-
Path-based (DeepState) - Best for bested state trees
-
Persistent State - Best for cross-session state
Best practices: always define a state interface, use
Partial<T>for updates, prefer immutable spreads, and normalise collections as{ byId, allIds }.
Difference between observer, singleton channel and BroadcastChannel
-
Observer — one object talking to its own watchers
-
Singleton Channel — any part of your app talking to any other part, within the same tab
-
BroadcastChannel — any tab talking to any other tab in the same browser
In practice, you’d often use all three together: a BroadcastChannel receives a
cross-tab message → publishes onto a singleton Channel<T> → individual
components subscribed via the Observer pattern react to the update.
TypeScript-Specific Best Practices
-
Always define a state interface — never use any for your state shape
-
Use
Partial<T>for updates — so you only pass the fields you’re changing -
Prefer immutable updates — spread operators
({ ...state, ...updates })instead of mutating directly -
Normalize entity collections — store arrays as
{ byId: Record<id, T>, allIds: id[] }for O(1) lookups -
Use
localStoragewith version migrations — persist state but include a_versionfield so you can safely migrate old data