NgRx vs observable services: Exploring application solutions

Discover the differences between NgRx and observable services when it comes to data flow handling for your application

Updated January 12, 2024

With regards to maintaining state in your application, there is beauty in having a single source of truth, but that can come at the cost of troublesome overhead.

Just like all things in life, in development you cannot have your cake and eat it too. Of course it would be perfect to have a solution that helps common problems devs face, such as event soup and change detection, without any extra work. But alas, ‘tis not the way of the world.

Explore #LifeAtCapitalOne

Startup-like innovation with Fortune 100 capabilities.

Two popular solutions for maintaining state and optimizing data flow that I’ll be talking about today are NgRx, with its redux-styled approach and observable services, which contain a desired state for a “slice” of your app in which subscriptions can be created.

For this article, I have implemented a mini chat application to demonstrate the differences in code between NgRx and observable services. I will also be discussing my experience implementing both of them.

What is NgRx?

There is no way I can improve upon the definition from the NgRx documentation, so let me quote them directly -

“NgRx is a framework for building reactive applications in Angular. NgRx provides libraries for:

  • Managing global and local state.
  • Isolation of side effects to promote a cleaner component architecture.
  • Entity collection management.
  • Integration with the Angular Router.
  • Developer tooling that enhances developer experience when building many different types of applications.”

What are observable services?

An observable service in Angular is a singleton that can be injected into your application. It provides accessors to manipulate data (such as adding an item to an array) and storing data.

Our example mini chat app

It's time to see them both in action! Today we’ll  work with a basic “chat” app that consumes no service/backend API. With this app you can:

  1. View a list of preset channels
  2. Update a channel name
  3. Read messages from the in-memory cache
  4. Send a message
  5. Switch Channels

For each “feature” I’ll break down the differences between each implementation.

Structure of observable services vs structure of NgRx

Observable service:

Example code for channel and message services

NgRx

All that is required is running ng g s <service_name> twice, once for the channel service and then another time for the message service. Two services are required—one for channel-related logic and another for messages.

Example code for channel and message services divided from actions, adapters, effects, reducers, selectors and state

Quite a bit more work has to be done here - and keep in mind, this is excluding specs for the reducers and effects. Each file generated contains code that has its own, specific responsibility. There are many files, but at least I’m following #singleresponsibilityprinciple.

Feature breakdown: Observable service vs NgRx

Here we'll discuss the difference in approaches for each feature.

Feature 1: View a list of preset channels

Observable service:

I have a list of channels which is stored in a local member variable in the ChannelService. I get the channels and channel updates by listening on the channelsChanged$ observable.

Finally, in the component, I subscribe to the channels.

NgRx:

Firstly, I define what the state would look like, then provide default values:

Then I have to create a way to get the channels, which is accomplished via a selector. This can be thought of as a “query”:

Finally, in the component I tell the store to get this slice of data by using the selector I created.

this.selectedChannelId$ =

store.pipe(select(selectCurrentChannelId));

Not too bad.

Feature 2: Update channel name

Observable service:

  1. Create the updateChannel method in the service.
  2. Call the method from the component.

View an example of this method here.

Note how the spread operator is used. This triggers change detection by returning a new array.

NgRx:

  1. Create an UpdateChannel action.
  2. Add logic in the reducer to update the corresponding channel.
  3. Dispatch the UpdateChannel action in the component.

I know what channel to update this time around since I hardcoded some IDs for the channels. I didn’t have to update the selected channel as the reference to the channel didn’t change.

Feature 3: Read messages

Observable service:

  1. Create a MessageService.
  2. To get messages, I use channel ids as keys and the array of messages as values in a messageDB variable. The messages string array is used to track messages for the current channel and handles pushing and returning values in the messageSubject.
  3. Subscribe to the messages$ observable from the message list component.

Here I establish the messageDB as well as the messages getter and setter. I also get messages for the current channel from the messageDB which happens when the user changes channels (example).

Example of the channel in the component.

NgRx:

This is a bit more complicated. However, it makes sense once all of the pieces are put together. The heart of the logic is the EntityAdapter.

  1. Create the entity adapter and set what the primary key will be. In this case, I want the channel ID to be the primary key. The value will be an object that contains entities, which is where our messages array will be. I’m using a MessageContainer object that has  channelId and an array of Message. channelId serves as a unique identifier for the messages. View the entity adapter here

  2. Create the selectors. I had to utilize selector composition, which is a fancy way of saying I created a selector from selectors. I have a selector getting the selected channel ID, one for getting the list of MessageContainers (messages for each channel), then I use those two to get the messages for the selected channel. View the example of creating selectors here

  3. To get the messages, I just had to use the composed selector created in (2). View the example of this here.

Feature 4: Add message

Observable service:

  1. Add a function to the MessageService to add the message.

  2. Call this service function from the component.

 NgRx:

  1. Create an AddMessage action.

  2. Add a case in the message reducer to add the message to the message array in the entity corresponding to the selected channel ID.

  3. Dispatch the action from the component. View the example of this here.

Feature 5: Switch channels

Observable service:

  1. In the MessageService, add a selectedChannelId property. This gets set when getInitialMessagsForChannel(channelId: number) is called.

  2. When the user selects a channel, I keep track of that channel’s ID in both the ChannelService and this MessageService. When the value gets updated in the ChannelService, it updates the value in the MessageService in the function listed above. The downside to this is that it creates tight coupling.

See lines 24-26 here.:

NgRx:

  1. Create a SelectChannelId channel action and message action (remember, the selected channel id is being tracked in both states).
  2. Add a case in the reducer to set the selectedChannelId in the channel reducer and the message reducer.
  3. Create the selectChannel selector.
  4. Something new - creating a selectChannel$ effect. I want to set the selected channel id in the channel state, but I also need to in the message state. This is called side-effect (hence why NgRx gave it this name). The effect created dispatches a new action which is picked up by the message reducer so it can also update its selected channel id. The alternative approach requires coupling the Message state with the Channel state. However, this approach would be problematic because the message state relies  on a value from another state to compose its selectors, which is an anti-pattern.

Final thoughts on NgRx vs observable services

These are based on my personal experiences using both and may not be applicable to all teams or use cases.

Observable service pros and cons

Pros of observable service:

The observable service method had a much quicker implementation. It’s slimmer and has a very minimal learning curve. It’s rewarding getting something done relatively quickly.

Cons of observable service:

I ran into issues with how to handle storing messages across channels. I wanted the messages to be cached so when returning to a channel they were “already” there. I implemented a solution with a fake database using the Record type, ended up disliking it, then came up with the solution that ended up with the messageDB I created. Still not sure if there is a better approach.

I also ran into an issue where I had to couple the ChannelService and the MessageService when tracking the selected channel’s id. If only there was some central store they could read from.  🤔

I can see the services blowing up fast for large applications. This would necessitate careful planning regarding how and when the services should be created. You wouldn’t want to overload a single service

NgRx pros and cons

Pros of NgRx:

No tight coupling.

Each logical bit in the app flow was stored in its respective component (e.g. the reducers, effects, etc). This helped make sures there was no questions about what should go where in the code.This in turn helped breakdown the app flow so you only have to focus on pieces of the puzzle at any point in time, instead of trying to keep large pieces of the flow in short term memory, like in the case of a large observable service.

Unidirectional data flow and clean handling of side effects. This ties in with the first point, but having a mechanism to handle side effects and help minimize event soup helps keep the code clean.

Cons of NgRx:

(Story Time - tl;dr it took way longer) I have been working with a mature project that uses NgRx, and this still took longer than the observable service implementation. I never worked on a project setting up the store (all the actions, state, selectors, effects, and reducers) from scratch, so there was a learning curve setting everything up. At one point I had to learn a new concept called an Entity Adapter (used to get messages given a specified channel ID), and that had a learning curve to it as well.

To put things into perspective, I planned out the app structure, created the components, created the UI designs and added in the observable service logic first and that whole process was still faster than adding in ONLY the NgRx implementation (the NgRx implementation re-used the UI and largely re-used the components).

However, that does not necessarily mean NgRx is worse, it is just because it took longer! This was likely more of an indication of the two learning curves I had to go through.

More cons of NgRx:

  • In a large application, it can become difficult to follow the series of actions/effects/reductions Upon getting acquainted with the code base, this becomes clear, but it still leads to a longer learning curve for new devs revisiting code written a while ago.
  • All the files. As mentioned before, there is no way around this. It’s just imperative that the folder structure is set up in the best way possible to allow easy navigation. This becomes increasingly important with larger code bases, as the number of files will increase exponentially.

  • Writing more code overall. You have to write the actions, effects, reducers and selectors, then write tests for each. However, because these components are all decoupled and broken down into smaller, pure functions, it makes writing tests easier and makes the tests cleaner.

Choosing between utilizing NgRx or observable service

You’ve likely heard this repeatedly, but use whatever works for your team. Asking yourself the right questions can help here. If nobody on your team has experience working with NgRx/redux, but you want to use it, can you afford the extra time to learn it? 

Does your organization require automated unit tests (e.g. Karma, Cypress), or do you depend on manual testing to get by? There are already many files and components to NgRx and each one will need to have an associated unit test, so you will need to keep in mind the extra work required to maintain this large test suite.


Michael McKenna, Software Engineer

Lover of all things tech and engineering. Full stack developer with an inclination for front end development. Sports enthusiast and triathlete on the side.

Related Content

Top down view of two adjacent and open filing cabinets showing two rows of slightly messy files and papers
Article | April 17, 2023 |7 min read
Article | January 22, 2019
Cloud illustration
Article | July 24, 2023