Tutorial

3 Ways to Pass Async Data to Angular 2+ Child Components

Updated on September 15, 2020
    author

    Jecelyn Yeen

    3 Ways to Pass Async Data to Angular 2+ Child Components

    Let’s start with a common use case. You have some data you get from external source (e.g. by calling API). You want to display it on screen.

    However, instead of displaying it on the same component, you would like to pass the data to a child component to display.

    The child component might has some logic to pre-process the data before showing on screen.

    Our Example

    For example, you have a blogger component that will display blogger details and her posts. Blogger component will gets the list of posts from API.

    Instead of writing the logic of displaying the posts in the blogger component, you want to reuse the posts component that is created by your teammate, what you need to do is pass it the posts data.

    The posts component will then group the posts by category and display accordingly, like this:

    blogger and posts

    Isn’t That Easy?

    It might look easy at the first glance. Most of the time we will initiate all the process during our component initialization time - during ngOnInit life cycle hook (refer here for more details on component life cycle hook).

    In our case, you might think that we should run the post grouping logic during ngOnInit of the posts component.

    However, because the posts data is coming from server, when the blogger component passes the posts data to posts component, the posts component ngOnInit is already fired before the data get updated. Your post grouping logic will not be fired.

    How can we solve this? Let’s code!

    Our Post Interfaces and Data

    Let’s start with interfaces.

    // post.interface.ts
    
    // each post will have a title and category
    export interface Post {
        title: string;
        category: string;
    }
    
    // grouped posts by category
    export interface GroupPosts {
        category: string;
        posts: Post[];
    }
    

    Here is our mock posts data assets/mock-posts.json.

    
    [
        { "title": "Learn Angular", "type": "tech" },
        { "title": "Forrest Gump Reviews", "type": "movie" },
        { "title": "Yoga Meditation", "type": "lifestyle" },
        { "title": "What is Promises?", "type": "tech" },
        { "title": "Star Wars Reviews", "type": "movie" },
        { "title": "Diving in Komodo", "type": "lifestyle" }
    ]
    
    

    Blogger Component

    Let’s take a look at our blogger component.

    // blogger.component.ts
    
    import { Component, OnInit, Input } from '@angular/core';
    import { Http } from '@angular/http';
    import { Post } from './post.interface';
    
    @Component({
        selector: 'bloggers',
        template: `
            <h1>Posts by: {{ blogger }}</h1>
            <div>
                <posts [data]="posts"></posts>
            </div>
        `
    })
    export class BloggerComponent implements OnInit {
    
        blogger = 'Jecelyn';
        posts: Post[];
    
        constructor(private _http: Http) { }
    
        ngOnInit() { 
            this.getPostsByBlogger()
                .subscribe(x => this.posts = x);
        }
    
        getPostsByBlogger() {
            const url = 'assets/mock-posts.json';
            return this._http.get(url)
                .map(x => x.json());
        }
    }
    

    We will get our mock posts data by issuing a HTTP GET call. Then, we assign the data to posts property. Subsequently, we bind posts to posts component in our view template.

    Please take note that, usually we will perform HTTP call in service. However, since it’s not the focus of this tutorial (to shorten the tutorial), we will do that it in the same component.

    Posts Component

    Next, let’s code out posts component.

    // posts.component.ts
    
    import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
    import { BehaviorSubject } from 'rxjs/BehaviorSubject';
    import { Post, GroupPosts } from './post.interface';
    
    @Component({
        selector: 'posts',
        template: `
        <div class="list-group">
            <div *ngFor="let group of groupPosts" class="list-group-item">
                <h4>{{ group.category }}</h4>
                <ul>
                    <li *ngFor="let post of group.posts">
                        {{ post.title }}
                    </li>
                </ul>
            <div>
        </div>
        `
    })
    export class PostsComponent implements OnInit, OnChanges {
    
    	@Input()
    	data: Post[];
    
    	groupPosts: GroupPosts[];
    
    	ngOnInit() {
    	}
    
    	ngOnChanges(changes: SimpleChanges) {
    	}
    
    	groupByCategory(data: Post[]): GroupPosts[] {
    		// our logic to group the posts by category
    		if (!data) return;
    		
    		// find out all the unique categories
    		const categories = new Set(data.map(x => x.category));
    		
    		// produce a list of category with its posts
    		const result = Array.from(categories).map(x => ({
    			category: x,
    			posts: data.filter(post => post.category === x)
    		}));
    
    		return result;
    	}
    }
    

    We have an input called data which will receive the posts data from parent component. In our case, blogger component will provide that.

    You can see that we implement two interfaces OnInit and OnChanges. These are the lifecycle hooks that Angular provide to us. We have not done anything in both ngOnInit and ngOnChanges just yet.

    The groupByCategory function is our core logic to group the posts by category. After the grouping, we will loop the result and display the grouped posts in our template.

    Remember to import these components in you module (e.g. app.module.ts) and add it under declarations.

    Save and run it. You will see a pretty empty page with the blogger name only. That’s because we have not code our solution yet.

    Solution 1: Use *ngIf

    Solution one is the easiest. Use *ngIf in blogger component to delay the initialization of posts components. We will bind the post component only if the posts variable has a value. Then, we are safe to run our grouping logic in posts component ngOnInit.

    Our blogger component:

    // blogger.component.ts
    
    ...
        template: `
            <h1>Posts by: {{ blogger }}</h1>
            <div *ngIf="posts">
                <posts [data]="posts"></posts>
            </div>
        `
    ...
    

    Our posts component.

    // posts.component.ts
    
    ...
        ngOnInit() {
            // add this line here
            this.groupPosts = this.groupByCategory(this.data);
        }
    ...
    

    A few things to note:

    • Since the grouping logic runs in ngOnInit, that means it will run only once. If there’s any future updates on data (passed in from blogger component), it won’t trigger again.
    • Therefore, if someone change the posts: Post[] property in the blogger component to posts: Post[] = [], that means our grouping logic will be triggered once with empty array. When the real data kicks in, it won’t be triggered again.

    Solution 2: Use ngOnChanges

    ngOnChanges is a lifecycle hook that run whenever it detects changes to input properties. That means it’s guaranteed that everytime data input value changed, our grouping logic will be triggered if we put our code here.

    Please revert all the changes in previous solution

    Our blogger component, we don’t need *ngIf anymore.

    // blogger.component.ts
    
    ...
        template: `
            <h1>Posts by: {{ blogger }}</h1>
            <div>
                <posts [data]="posts"></posts>
            </div>
        `
    ...
    

    Our posts component

    // posts.component.ts
    
    ...
        ngOnChanges(changes: SimpleChanges) {
            // only run when property "data" changed
            if (changes['data']) {
                this.groupPosts = this.groupByCategory(this.data);
            }
        }
    ...
    

    Please notes that changes is a key value pair object. The key is the name of the input property, in our case it’s data. Whenever writing code in ngOnChanges, you may want to make sure that the logic run only when the target data changed, because you might have a few inputs.

    That’s why we run our grouping logic only if there are changes in data.

    One thing I don’t like about this solution is that we lose the strong typing and need to use magic string “data”. In case we change the property name data to something else, we need to remember to change this as well.

    Of course we can defined another interface for that, but that’s too much work.

    Solution 3: Use RxJs BehaviorSubject

    We can utilize RxJs BehaviorSubject to detect the changes. I suggest you take a look at the unit test of the official document here before we continue.

    Just assume that BehaviorSubject is like a property with get and set abilities, plus an extra feature; you can subscribe to it. So whenever there are changes on the property, we will be notified, and we can act on that. In our case, it would be triggering the grouping logic.

    Please revert all the changes in previous solution

    There are no changes in our blogger component:

    // blogger.component.ts
    
    ...
        template: `
            <h1>Posts by: {{ blogger }}</h1>
            <div>
                <posts [data]="posts"></posts>
            </div>
        `
    ...
    

    Let’s update our post component to use BehaviorSubject.

    // posts.component.ts
    
    ...
    	// initialize a private variable _data, it's a BehaviorSubject
    	private _data = new BehaviorSubject<Post[]>([]);
    
    	// change data to use getter and setter
    	@Input()
    	set data(value) {
    		// set the latest value for _data BehaviorSubject
    		this._data.next(value);
    	};
    
    	get data() {
    		// get the latest value from _data BehaviorSubject
    		return this._data.getValue();
    	}
    	
    	ngOnInit() {
    		// now we can subscribe to it, whenever input changes, 
    		// we will run our grouping logic
    		this._data
    			.subscribe(x => {
    				this.groupPosts = this.groupByCategory(this.data);
    			});
    	}
    ...
    
    

    First of all, if you are not aware, Javacript supports getter and setter like C# and Java, check MDN for more info. In our case, we split the data to use getter and setter. Then, we have a private variable _data to hold the latest value.

    To set a value to BehaviorSubject, we use .next(theValue). To get the value, we use .getValue(), as simple as that.

    Then during component initialization, we subscribe to the _data, listen to the changes, and call our grouping logic whenever changes happens.

    Take a note for observable and subject, you need to unsubscribe to avoid performance issues and possible memory leaks. You can do it manually in ngOnDestroyor you can use some operator to instruct the observable and subject to unsubscribe itself once it meet certain criteria.

    In our case, we would like to unsubscribe once the groupPosts has value. We can add this line in our subscription to achieve that.

    // posts.component.ts
    
    ...
    	ngOnInit() {
    		this._data
    			// add this line
    			// listen to data as long as groupPosts is undefined or null
    			// Unsubscribe once groupPosts has value
    			.takeWhile(() => !this.groupPosts)
    			.subscribe(x => {
    				this.groupPosts = this.groupByCategory(this.data);
    			});
    	}
    ...
    
    

    With this one line .takeWhile(() => !this.groupPosts), it will unsubscribe automatically once it’s done. There are other ways to unsubscribe automatically as well, e.g take, take Util, but that’s beyond this topic.

    By using BehaviorSubject, we get strong typing, get to control and listen to changes. The only downside would be you need to write more code.

    Which One Should I Use?

    The famous question comes with the famous answer: It depends.

    Use *ngIf if you are sure that your changes run only once, it’s very straightforward. Use ngOnChanges or BehaviorSubject if you want to listen to changes continuously or you want guarantee.

    That’s it. Happy Coding!

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Jecelyn Yeen

    author

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    2 Comments
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Brilliant article. Actually, I was able to pass data without using any of these solutions. If the data is being used inside a function in the Posts component. And the function is not called immediately, then the data will be available without the need for any assistance.

    However, I must say, I prefer your ngOnChanges solution.

    Ugh, this was heaven sent. Great explanation. Thanks, y’all. Possibly consider switching to Stackblitz for demos?

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.