When multiple, unrelated components need access to the same state, a shared, injectable service is usually the right tool, well before reaching for a heavier state management library. This lesson covers building one well.
Why Put State in a Service?
A singleton service, provided at the root injector, is naturally shared: every component that injects it receives the exact same instance, and therefore the exact same underlying state, without any additional wiring.
This makes services a lightweight, built-in "store" for state that multiple, otherwise unrelated components need to read and update, like a shopping cart, the current user, or UI-wide theme preferences.
Keep the writable signal private; expose a readonly view (asReadonly()) for consumers.
Group related state into a single object signal when fields are logically related and often updated together.
Public methods (like add(), someAction()) are the only sanctioned way to change the state from outside.
computed() values derived from the private signal can be exposed publicly just like the state itself.
Service State Cheatsheet
Patterns for building a clean, shared state service.
Pattern
Purpose
Private writable signal
Keeps write access under the service's control
Public .asReadonly() signal
Lets consumers read state without mutating it directly
Public methods (add, remove, ...)
The sanctioned way to change state
computed() public getters
Expose derived values without extra manual syncing
BehaviorSubject + .asObservable()
RxJS-based alternative to the signal pattern above
A Complete Signal-Based Store Example
Combining a private writable signal, a public readonly view, computed derived values, and clear action methods produces a small, predictable store without any external library.
The same pattern works with a BehaviorSubject instead of a signal, useful in codebases still primarily RxJS-based, or when the state naturally needs RxJS operators (like debouncing updates).
Components inject the service and read its public state directly, no manual wiring, subscriptions, or props drilling required, even for completely unrelated components elsewhere in the tree.
Public methods on the service are the only sanctioned way to change shared state.
The same pattern works with either signals or BehaviorSubject, choose based on your codebase's conventions.
Pro Tip
Design service state APIs around clear, named actions (add(), remove(), clear()) rather than exposing a generic setState() method; it makes every state change traceable to a specific, intentional call site.
You now understand service-based shared state. Next, learn about NgRx, a dedicated state management library for larger applications.