How to share data between components in Angular: A shopping cart example
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
@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
To illustrate the power of Services which leverage Observables and Behavior Subjects, we’ll set up a basic shopping cart application. This shopping cart will feature a list of items, a subtotals section, and a summary displaying the number of items in the cart. The app will also allow users to edit the quantity of an item, as well as the ability to delete items.
Overall, the shopping cart will look like this:
The app will be broken into the following components / services:
AppComponent, which will hold the scaffolding of the app.
SummaryComponent, which will display a shopping cart summary in the header with the number of items in the cart.
ItemsComponent, which will display the list of items in the cart.
SubTotalComponent, which will display the subtotal.
- Finally, and most importantly, a
ShoppingCartService, which will to hold the state and business logic to share across all components.
Create the app
First, install the Angular CLI (if you haven’t already) by opening your terminal and running:
$ 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
To save some time, we’ll use Bootstrap’s CSS library and the FontAwesome web font to style our project, so add a link straight to their CDNs to load them in
Add basic HTML scaffolding to AppComponent
AppComponent view will hold the basic scaffolding of our app, so replace the default HTML in
app.component.html with the code below (we’ll start out with placeholders where our components will go, and will replace them as we move along):
At this point, after saving your changes and reloading, your app should look like this:
Create the data interfaces
Our app will have a shopping cart which will contain items, and each item will contain a SKU (Stock Keeping Unit), or product item. The interfaces to structure this data will be declared in our
models.ts file as follows:
Create shopping cart data file
For this app, our sample data will come from
/assets/data.json, which is a JSON file which will contain a shopping cart with two items. Go ahead and add such file in the
assets/ directory of the application with the data below:
Create the shopping cart service
ShoppingCartService will be the brains of our application, and will serve two main purposes:
- It will hold the basic state of the shopping cart in its
shoppingCart$property. This property will be a
BehaviorSubjectwhich will emit a
- 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
.valueproperty without the need of a subscription. This is useful when we need to treat the
BehaviorSubjectas a synchronous variable for read-only purposes.
- Lastly (and most importantly), since a
BehaviorSubjectis 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
- 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
ItemsComponent will display the list of items in the shopping cart (pro tip: the
--export flag will automatically add the new component to the list of
exports in the most immediate module, in this case
AppModule, saving us the work of having to add it manually).
$ 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,
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
Populate the list of shopping cart items with data from the shopping cart
Here’s where we’ll start using the
ShoppingCartService to manage our app’s state.
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
BehaviorSubject, and will return an Observable which emits a list of items
Observable<Item> every time
shoppingCart$ is updated:
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
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
SubtotalComponent will display the subtotal of the items in the cart, the number of items, and the “Proceed to Checkout” button. Certainly, in a production implementation, we’d most likely have a dedicated component for this button to hold any complex display / business logic, but we’ll just display it in here for the sake of brevity.
$ ng generate component subtotal --export
count$, will hold Observables which will emit numbers,
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
0, then the cart is empty, so the user should not be able to proceed to checkout).
ShoppingCartService , add two methods,
getCount() to retrieve them.
Similar to the
getItems() method, the
getSubtotal() method will be a projection derived from the
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:
getCount() method will also be a projection derived the from
BehaviorSubject (you start seeing a pattern here, right?) that will return and Observable which emits the count of all the items in the cart,
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 on
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
SummaryComponent will be placed in the header and will display a shopping cart icon with the number of items in the cart.
$ ng generate component summary --export
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 in
SubtotalComponent, 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
Each item has a
<select> input field which reflects its quantity, with values ranging from 1 to 3 (please note that this limitation is for illustrative purposes). Our next step is handle any changes in quantity and update the shopping cart accordingly.
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 the
Observables we’ve created from
shoppingCart$, such as the ones in the
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
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
SummaryComponent, in a great example of multiple components communicating via a common service.
Delete an item
As a final feature in our app, we’ll add the option for users to delete items by pressing the “Delete” link on an item.
For this, we’ll add a new
deleteItem() method to the
ShoppingCartService. 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
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.
With this basic shopping cart application we’ve explored the power of sharing both the data and business logic of your application between components using services and Observables, and more specifically, Behavior Subjects. Given the replicability of this approach, it can quickly be used to standardize your approach to state management across your Angular application, making it easier to scale and maintain in the future.
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
- By using RxJS operators, we can create projections from the
BehaviorSubjectholding 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!