Since signals were integrated into Angular 16, state management has split into two divergent paths. On one side, NgRx SignalStore offers a lightweight, opinionated abstraction built on native signals. On the other, signal-based services provide a minimalist approach with no external dependencies. These two solutions don't address the same problems, and confusing their use cases leads to fragile, hard-to-maintain architectures. This article demystifies the concrete differences, performance trade-offs, and how to make the right decision based on your application context.
Architecture and Philosophy: Two Opposing Visions
NgRx SignalStore is an abstraction layer built by the NgRx team to normalize state management with signals. It provides explicit structure: typed stores, reactive selectors, integrated effects, and a declarative API for mutations. Each store is a class annotated with @signalStore() that exposes public signals, computed signals for derivations, and methods for actions. This approach echoes Redux or Vuex, but lighter and without heavy middleware. Conversely, a signal-based service is just a regular class containing private signals and exposing getters or methods to manipulate them. It's minimal: no conventions, no decorators, no infrastructure. A developer creates a UserService with a $user signal and that's it. This philosophical difference explains why the two approaches are not interchangeable.
When NgRx SignalStore Makes Sense
NgRx SignalStore excels in medium to large applications with complex business logic and multiple state domains. Imagine an e-commerce app: you have a store for products, one for the cart, one for orders, one for authentication. Each of these domains can have composable selectors, async effects (API calls), and conditional mutations. SignalStore allows you to structure this coherently and testably. Integrated effects facilitate side effect management: loading products on startup, validating a promo code, persisting the cart. You also benefit from the NgRx ecosystem: Redux DevTools for debugging, established patterns, and an active community. If your team has already used classic NgRx (with ngrx/store), the migration to SignalStore is progressive. Another use case: applications with complex shared state. When multiple distant components must react to the same data, and that data transforms differently depending on context, SignalStore provides a single, reliable source.
When a Signal-Based Service Suffices
Signal-based services shine in small applications, micro-applications, or isolated business domains. Take a simple todo list, a minimalist e-commerce cart, or an authentication widget. A TodoService with a $todos signal, an addTodo() method, and a computed $remainingCount signal is more than enough. No infrastructure, no external dependencies, no learning curve. The code is immediately readable to a junior. Signal-based services are also preferable when you need local flexibility: a dynamic form maintaining its own state, a list with client-side filtering, a modal with internal logic. These components don't need global normalization. Finally, if you're building an Angular component library (design system), exposing signal-based services rather than depending on NgRx makes your library lighter and more adaptable.
Performance and Bundle Footprint Comparison
On the bundle side, a signal-based service adds nothing: it's native Angular code. NgRx SignalStore adds approximately 15-20 KB gzipped (v18+), which is not huge but worth considering in a PWA. On reactivity, both approaches are equivalent: they rely on signals, so granular change detection and no zone.js by default. However, SignalStore allows better optimization in large applications: memoized selectors avoid unnecessary recalculations, and isolated effects prevent side effect entanglement. In a poorly structured signal-based service, you often see developers creating too many computed signals or watchers that trigger in cascade. The real performance difference emerges in production: a well-designed SignalStore application scales better than a heterogeneous collection of signal-based services, because the architecture enforces discipline.
Concrete Example: Cart Management
With SignalStore, a cart would look like this: a typed store with signals for items and total, computed signals for item count and tax, methods to add/remove items, and an effect to persist changes. The code is self-documenting and testable in isolation. With a signal-based service, you'd create a CartService with an $items signal, addItem() and removeItem() methods, and save logic inside the method. It's shorter, but if the cart had to also handle promo codes, discounts, multi-tab sync, and payment attempts, the service would quickly become a monolith hard to test.
The Common Pitfall: Over-Architecting with SignalStore
Many teams adopt NgRx SignalStore for applications that don't justify it. You see small projects with 5 complex stores to manage 3 simple forms. The result: more code to maintain, unnecessary learning curve, and complexity that slows onboarding. Conversely, some projects grow gradually and end up with a dozen duplicated, incoherent signal-based services. The right strategy: start simple with signal-based services, and migrate to SignalStore only when you identify code duplication, testability difficulties, or intertwined side effects. This incremental approach reduces architectural regrets.
Cohabitation and Hybrid Patterns
It's not uncommon to see SignalStore and signal-based services coexist. For example, a global store manages authentication and navigation, while signal-based services manage local form and filtered list state. This is a healthy approach: you centralize what needs to be, and keep the rest local. Hybrid patterns also allow progressive migration: replace a signal-based service with a SignalStore when complexity justifies the investment.
Operational Conclusion
Choosing between NgRx SignalStore and a signal-based service isn't a matter of personal preference, but problem adequacy. Ask yourself: does state need to be shared across multiple business domains? Are there complex, nested side effects? Does the team master Redux patterns? If yes, SignalStore. If no, if state is local and simple, a signal-based service suffices. In most cases, a modern Angular application benefits from a combination: SignalStore for global state, signal-based services for local state. Measure your actual complexity before adding a dependency, but don't let initial minimalism block you when architecture becomes uncontrollable. Angular provides the tools to evolve progressively.