How to share data between components in Angular: A shopping cart example

Vidal Quevedo
Aug 11 · 12 min read

If you have developed modern web applications in Angular, you have inevitably run into a situation in which you need to share data among components (from parent to child, child to parent, child to grandparent, or *gasp* sibling to sibling).

Angular offers many different ways to share data between components, such as the @Input() and @Output() pattern, or by accessing instances of another component using @ViewChild(). However, these approaches tend to be limited in their scope and can quickly become difficult to maintain, especially when data needs to be shared among components which are either deeply nested, or not immediately connected at all.

Fortunately, Services in Angular are a powerful feature to create common points of reference for sharing both data and business logic among components. In addition, when combined with Observables and, more specifically, BehaviorSubjects, we can supercharge them further to create stateful, reactive services which can help us synchronize the state more efficiently across the whole application.

A shopping cart example

Overall, the shopping cart will look like this:

The app will be broken into the following components / services:

  • The AppComponent, which will hold the scaffolding of the app.
  • A SummaryComponent, which will display a shopping cart summary in the header with the number of items in the cart.
  • An ItemsComponent, which will display the list of items in the cart.
  • A SubTotalComponent, which will display the subtotal.
  • Finally, and most importantly, aShoppingCartService, which will to hold the state and business logic to share across all components.

The setup

Create the app

$ npm install -g @angular/cli

Next, use the Angular CLI to create a new shopping-cart app (select “N” for routing and “SCSS” as the stylesheet format). Next, cd to the new shopping-cart/ and start the app. Once the app is loaded, go to localhost:4200 to open it.

$ ng new shopping-cart
$ cd shopping-cart
$ npm start

Add Bootstrap CSS and FontAwesome

Add basic HTML scaffolding to AppComponent

At this point, after saving your changes and reloading, your app should look like this:

Create the data interfaces

Create shopping cart data file

The features

Create the shopping cart service

  1. It will hold the basic state of the shopping cart in its shoppingCart$ property. This property will be a BehaviorSubject which will emit a ShoppingCart data object.
  2. It will hold the business logic needed to calculate and process data, as well as handle events for editing items in the shopping cart.

Now, why use a BehaviorSubject to hold the state and be the single source of truth of the application? A BehaviorSubject is a type of Observable with native properties which make it ideal for setting the state of an application in a reactive way:

  • It requires an initial value when instantiated, which allows us to set a default value for our state.
  • It emits its latest value (default or otherwise) on subscription, which allows us to start working with its emitted data right away.
  • It allows us to get “snapshots” of its latest value via its .getValue() method or .value property without the need of a subscription. This is useful when we need to treat the BehaviorSubject as a synchronous variable for read-only purposes.
  • Lastly (and most importantly), since a BehaviorSubject is a type of Observable, it allows us to use RxJS operators to create new projections to derive more meaningful data without ever modifying the original state. Since these projections are also Observables, they can be the building blocks for creating a reactive API for our Service, which will then propagate changes the entire UI and help us keep all components seamlessly synchronized.

So, what are projections?

Simply put, creating a projection means taking the original data source (or parts of it) to create a new, more specialized data out of it without ever modifying the original data source itself. For example:

  • From the shopping cart data, we could create a projection to calculate the list of items in the shopping cart.
  • From the shopping cart data, we could create a projection to get the list of items in the shopping cart, and then add up the quantity of each item to calculate the total number of items in the cart.
  • From the shopping cart data, we could create a projection to extract the list of items, multiply each item’s quantity by its price, and then add them all up to get the subtotal of the shopping cart.

Now, an important point about these projections is that they are run automatically every time the original data source is updated.

In our case, this translates into using RxJS operators to create projections from the original data emitted by the source, a BehaviorSubject, and then subscribing to the resulting Observables to update the view in real time every time the source emits a new value.

With these base concepts in mind, let’s go ahead and create our reactive, projection-based, service (you’ll start seeing the patterns as we move along, I promise ;)):

$ ng generate service services/shopping-cart

Next, two important things to add to the new shopping-cart.service.ts:

  • We’ll declare the shoppingCart$ private property as a BehaviorSubject<ShoppingCart> object, and then initialize it with a default value representing an empty shopping cart in the constructor.
  • We’ll add a getShoppingCart() private method which will place an HTTP request to get the data in the constructor. On success, we’ll call the setShoppingCart() private method, in which the shoppingCart$ property will emit the new shopping cart data. On failure, we’ll simply display an error in the browser’s console.

Please note that, since the service uses the HttpClient to make HTTP requests, we need to add the HttpClientModule to the list of the imports in app.module.ts to make it available across our app:

Create the shopping cart items component

$ ng generate component items --export

In the new items.component.ts file, we’ll set the items$ property as an Observable which will emit a list of items, Observable<Items[]>:

In items.component.html, we’ll subscribe to items$ using the async pipe to display the list of items. Each item will have an image, title, price (using the currency pipe for formatting), and quantity, as well as a “delete” button for removing it from the cart. If the shopping cart is empty, the component will display a “Your shopping cart is empty.” message instead.

Now, let add the items component to app.component.html by replacing the “(Shopping cart items)” placeholder with <app-items></app-items>.

Populate the list of shopping cart items with data from the shopping cart

First, we need the ShoppingCartService to allow us to access the current list of items in the shopping cart. For this, we’ll add the getItems() method to shopping-cart.service.ts. This method will be a projection derived from the shoppingCart$ BehaviorSubject, and will return an Observable which emits a list of items Observable<Item[]> every time shoppingCart$ is updated:

The pluck operator is an RxJS operator which simply allows you to return a specific property to emit by the Observable. This is certainly handy when you know what property you need to get from the incoming data and don’t need to pass a whole data object along on each emission.

Next, let’s load the list of items in items.component.ts on ngOnInit()using the getItems()method from ShoppingCartService, making sure to first inject the shoppingCartService as a dependency in the component to access its functionality:

After saving and reloading, our app should now display the list of items as shown below:

Create the shopping cart subtotal component

$ ng generate component subtotal --export

Two properties, subTotal$ and count$, will hold Observables which will emit numbers, Observable<number>:

In subtotal.component.html, the count$ property will be used not only to display the number of items, but also to add basic “disabling” logic to the “Proceed to Checkout” button (i.e. when $count equals 0, then the cart is empty, so the user should not be able to proceed to checkout).

In ShoppingCartService , add two methods, getSubstotal() andgetCount() to retrieve them.

Similar to the getItems() method, the getSubtotal() method will be a projection derived from the shoppingCart$ BehaviorSubject, which means that it will be an Observable which emits a number Observable<number> every time shoppingCart$ is updated.

To calculate the subtotal, we’ll multiply each shopping cart item’s quantity by its SKU’s price, and then add up the results, like so:

Similarly, the getCount() method will also be a projection derived the from shoppingCart$ BehaviorSubject (you start seeing a pattern here, right?) that will return and Observable which emits the count of all the items in the cart, Observable<number>.

Now, we can hook these new methods to the SubTotalComponent to start emitting these values to the view. First, we need to inject the ShoppingCartService as a dependency in the constructor, and then assign calls to these methods onngOnInit() in subtotal.component.ts:

Finally, we’ll replace the “(Shopping cart subtotal)” placeholder with the <app-subtotal></app-subtotal> tags to display the subtotal.

After saving and reloading, our shopping cart’s subtotal area should now be with two (2) items and a subtotal of $119.98:

Create the shopping cart summary component

$ ng generate component summary --export

The SummaryComponent will have a count$ property. To populate it, we’ll initialize it in ngOnInit() with a reference to the getCount() method we’ve already created in ShoppingCartService. By doing this, we’ll reuse the same business logic and state we also used inSubtotalComponent, effectively taking advantage of using services to share data and functionality among components:

In the view, we’ll display the count along with the cart icon:

Finally, we’ll replace the “(Shopping cart summary)” placeholder with <app-summary></app-summary> to create the component.

After saving and reloading, our app should now display a cart icon on the header, along with the number 2, since we have two items in the cart on load:

And we now have created all the required components and powered them with the ShoppingCartService!!! Next, we’ll create a couple of methods to edit the state and have the view react to such changes instantaneously across all components without having to manually update their states.

Update an item’s quantity

In the ShoppingCartService, let’s create an updateQuantity() method for this purpose. This method will simply find the passed item by its id, and then update its quantity accordingly.

There are two important steps in here: first, we’ll create a copy of the current state in the by accessing the value property of the shoppingCart$ BehaviorSubject. Then, after finding the item and calculating its quantity, we’ll have shoppingCart$ emit this new value via its next() method. By doing this, all theObservables we’ve created from shoppingCart$, such as the ones in the getItems(), getTotals(), and getCount() methods, will emit those values as well to all of their subscribers, effectively updating the view across all components with the new state.

Please note that, in a production application, any event that edits the items of a shopping cart will most likely be processed by the back end, which will then provide an updated copy of the shopping cart data that we can use to refresh the front end. In our example, however, we’re editing the state only on the front end for illustrative purposes, as the set up for a supporting back end architecture is out of the scope of our article.

Now, let’s invoke updateQuantity() via a method of similar name in ItemsComponent, which will take the $event object to extract the selected value on each change event of the select quantity input element in the view:

Finally, let’s call the method on change:

After saving and reloading, go ahead and change the quantity of any item. While the change was generated in ItemsComponent, the quantity and subtotal are updated seamlessly in SubTotalsComponent and SummaryComponent, in a great example of multiple components communicating via a common service.

Delete an item

For this, we’ll add a new deleteItem() method to theShoppingCartService. which will take the id of the item to delete:

Notice how, as in the updateQuantity() method, we create a copy of the state first from the shoppingCart$ property, perform our delete operation on the copy, and pass the copy to shoppingCart$ to emit as its new value, once again updating all Observables based off that Behavior Subject and their subscriptions across all components.

Next, we’ll create a similarly named method in ItemsComponent to invoke the ShoppingCartService‘s deleteItem() method:

And finally, let’s call the method above when the user clicks the “Delete” button on any item:

After saving and reloading, now go ahead and delete an item. The item should disappear from the view and the count and subtotal update accordingly. Deleting all items should also display a “Your shopping cart is empty.” message.

Conclusion

So, a few takeaways:

  • Services in Angular are a powerful feature to create common points of reference for sharing both data and business logic among components, and act as the single source of truth powering an application.
  • BehaviorSubjects can be used as the starting point to create reactive services due to their native ability to allow us to get its latest emitted data with or without a subscription (via its .value property or .getValue() method).
  • By using RxJS operators, we can create projections from the BehaviorSubject holding our original state to derive more meaningful data without ever modifying the original state.
  • These projections can be used to create a reactive API for our Service, and components can then use it to share data and business logic across the application.

Thank you, and please feel free to share your comments or questions!

CodeX

Everything connected with Tech & Code. Follow to join our 500K+ monthly readers