One place for hosting & domains

      How To Handle Async Data Loading, Lazy Loading, and Code Splitting with React


      The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

      Introduction

      As a JavaScript web developer, asynchronous code gives you the ability to run some parts of your code while other parts are still waiting for data or resolving. This means that important parts of your app will not have to wait for less important parts before they render. With asynchronous code you can also update your application by requesting and displaying new information, giving users a smooth experience even when long functions and requests are processing in the background.

      In React development, asynchronous programming presents unique problems. When you use React functional components for example, asynchronous functions can create infinite loops. When a component loads, it can start an asynchronous function, and when the asynchronous function resolves it can trigger a re-render that will cause the component to recall the asynchronous function. This tutorial will explain how to avoid this with a special Hook called useEffect, which will run functions only when specific data changes. This will let you run your asynchronous code deliberately instead of on each render cycle.

      Asynchronous code is not just limited to requests for new data. React has a built-in system for lazy loading components, or loading them only when the user needs them. When combined with the default webpack configuration in Create React App, you can split up your code, reducing a large application into smaller pieces that can be loaded as needed. React has a special component called Suspense that will display placeholders while the browser is loading your new component. In future versions of React, you’ll be able to use Suspense to load data in nested components without render blocking.

      In this tutorial, you’ll handle asynchronous data in React by creating an app that displays information on rivers and simulates requests to Web APIs with setTimeout. By the end of this tutorial, you’ll be able to load asynchronous data using the useEffect Hook. You’ll also be able to safely update the page without creating errors if the component unmounts before data resolution. Finally, you’ll split a large application into smaller parts using code splitting.

      Prerequisites

      Step 1 — Loading Asynchronous Data with useEffect

      In this step, you’ll use the useEffect Hook to load asynchronous data into a sample application. You’ll use the Hook to prevent unnecessary data fetching, add placeholders while the data is loading, and update the component when the data resolves. By the end of this step, you’ll be able to load data with useEffect and set data using the useState Hook when it resolves.

      To explore the topic, you are going to create an application to display information about the longest rivers in the world. You’ll load data using an asynchronous function that simulates a request to an external data source.

      First, create a component called RiverInformation. Make the directory:

      • mkdir src/components/RiverInformation

      Open RiverInformation.js in a text editor:

      • nano src/components/RiverInformation/RiverInformation.js

      Then add some placeholder content:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      import React from 'react';
      
      export default function RiverInformation() {
        return(
          <div>
            <h2>River Information</h2>
          </div>
        )
      }
      

      Save and close the file. Now you need to import and render the new component to your root component. Open App.js:

      • nano src/components/App/App.js

      Import and render the component by adding in the highlighted code:

      async-tutorial/src/components/App/App.js

      import React from 'react';
      import './App.css';
      import RiverInformation from '../RiverInformation/RiverInformation';
      
      function App() {
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <RiverInformation />
          </div>
        );
      }
      
      export default App;
      

      Save and close the file.

      Finally, in order to make the app easier to read, add some styling. Open App.css:

      • nano src/components/App/App.css

      Add some padding to the wrapper class by replacing the CSS with the following:

      async-tutorial/src/components/App/App.css

      .wrapper {
          padding: 20px
      }
      

      Save and close the file. When you do, the browser will refresh and render the basic components.

      Basic Component, 1

      In this tutorial, you’ll make generic services for returning data. A service refers to any code that can be reused to accomplish a specific task. Your component doesn’t need to know how the service gets its information. All it needs to know is that the service will return a Promise. In this case, the data request will be simulated with setTimeout, which will wait for a specified amount of time before providing data.

      Create a new directory called services under the src/ directory:

      This directory will hold your asynchronous functions. Open a file called rivers.js:

      • nano src/services/rivers.js

      Inside the file, export a function called getRiverInformation that returns a promise. Inside the promise, add a setTimeout function that will resolve the promise after 1500 milliseconds. This will give you some time to see how the component will render while waiting for data to resolve:

      async-tutorial/src/services/rivers.js

      export function getRiverInformation() {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve({
              continent: 'Africa',
              length: '6,650 km',
              outflow: 'Mediterranean'
            })
          }, 1500)
        })
      }
      

      In this snippet, you are hard-coding the river information, but this function will be similar to any asynchronous functions you may use, such as an API call. The important part is that the code returns a promise.

      Save and close the file.

      Now that you have a service that returns the data, you need to add it to your component. This can sometimes lead to a problem. Suppose you called the asynchronous function inside of your component and then set the data to a variable using the useState Hook. The code will be like this:

      import React, { useState } from 'react';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation() {
        const [riverInformation, setRiverInformation] = useState({});
      
        getRiverInformation()
        .then(d => {
          setRiverInformation(d)
        })
      
        return(
          ...
        )
      }
      

      When you set the data, the Hook change will trigger a components re-render. When the component re-renders, the getRiverInformation function will run again, and when it resolves it will set the state, which will trigger another re-render. The loop will continue forever.

      To solve this problem, React has a special Hook called useEffect that will only run when specific data changes.

      The useEffect Hook accepts a function as the first argument and an array of triggers as the second argument. The function will run on the first render after the layout and paint. After that, it will only run if one of the triggers changes. If you supply an empty array, it will only run one time. If you do not include an array of triggers, it will run after every render.

      Open RiverInformation.js:

      • nano src/components/RiverInformation/RiverInformation.js

      Use the useState Hook to create a variable called riverInformation and a function called setRiverInformation. You’ll update the component by setting the riverInformation when the asynchronous function resolves. Then wrap the getRiverInformation function with useEffect. Be sure to pass an empty array as a second argument. When the promise resolves, update the riverInformation with the setRiverInformation function:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      import React, { useEffect, useState } from 'react';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation() {
        const [riverInformation, setRiverInformation] = useState({});
      
        useEffect(() => {
         getRiverInformation()
         .then(data =>
           setRiverInformation(data)
         );
        }, [])
      
      
        return(
          <div>
            <h2>River Information</h2>
            <ul>
              <li>Continent: {riverInformation.continent}</li>
              <li>Length: {riverInformation.length}</li>
              <li>Outflow: {riverInformation.outflow}</li>
            </ul>
          </div>
        )
      }
      

      After the asynchronous function resolves, update an unordered list with the new information.

      Save and close the file. When you do the browser will refresh and you’ll find the data after the function resolves:

      River Information Updating After Load, 2

      Notice that the component renders before the data is loaded. The advantage with asynchronous code is that it won’t block the initial render. In this case, you have a component that shows the list without any data, but you could also render a spinner or a scalable vector graphic (SVG) placeholder.

      There are times when you’ll only need to load data once, such as if you are getting user information or a list of resources that never change. But many times your asynchronous function will require some arguments. In those cases, you’ll need to trigger your use useEffect Hook whenever the data changes.

      To simulate this, add some more data to your service. Open rivers.js:

      • nano src/services/rivers.js

      Then add an object that contains data for a few more rivers. Select the data based on a name argument:

      async-tutorial/src/services/rivers.js

      const rivers = {
       nile: {
         continent: 'Africa',
         length: '6,650 km',
         outflow: 'Mediterranean'
       },
       amazon: {
         continent: 'South America',
         length: '6,575 km',
         outflow: 'Atlantic Ocean'
       },
       yangtze: {
         continent: 'Asia',
         length: '6,300 km',
         outflow: 'East China Sea'
       },
       mississippi: {
         continent: 'North America',
         length: '6,275 km',
         outflow: 'Gulf of Mexico'
       }
      }
      
      export function getRiverInformation(name) {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(
              rivers[name]
            )
          }, 1500)
        })
      }
      

      Save and close the file. Next, open App.js so you can add more options:

      • nano src/components/App/App.js

      Inside App.js, create a stateful variable and function to hold the selected river with the useState Hook. Then add a button for each river with an onClick handler to update the selected river. Pass the river to RiverInformation using a prop called name:

      async-tutorial/src/components/App/App.js

      import React, { useState } from 'react';
      import './App.css';
      import RiverInformation from '../RiverInformation/RiverInformation';
      
      function App() {
        const [river, setRiver] = useState('nile');
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <button onClick={() => setRiver('nile')}>Nile</button>
            <button onClick={() => setRiver('amazon')}>Amazon</button>
            <button onClick={() => setRiver('yangtze')}>Yangtze</button>
            <button onClick={() => setRiver('mississippi')}>Mississippi</button>
            <RiverInformation name={river} />
          </div>
        );
      }
      
      export default App;
      

      Save and close the file. Next, open RiverInformation.js:

      • nano src/components/RiverInformation/RiverInformation.js

      Pull in the name as a prop and pass it to the getRiverInformation function. Be sure to add name to the array for useEffect, otherwise it will not rerun:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      import React, { useEffect, useState } from 'react';
      import PropTypes from 'prop-types';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation({ name }) {
        const [riverInformation, setRiverInformation] = useState({});
      
        useEffect(() => {
          getRiverInformation(name)
          .then(data =>
            setRiverInformation(data)
          );
        }, [name])
      
      
        return(
          <div>
            <h2>River Information</h2>
            <ul>
              <li>Continent: {riverInformation.continent}</li>
              <li>Length: {riverInformation.length}</li>
              <li>Outflow: {riverInformation.outflow}</li>
            </ul>
          </div>
        )
      }
      
      RiverInformation.propTypes = {
       name: PropTypes.string.isRequired
      }
      

      In this code, you also added a weak typing system with PropTypes, which will make sure that the prop is a string.

      Save the file. When you do, the browser will refresh and you can select different rivers. Notice the delay between when you click and when the data renders:

      Update river information, 3

      If you had left out the name prop from the useEffect array, you would receive a build error in the browser console. It would be something like this:

      Error

      Compiled with warnings. ./src/components/RiverInformation/RiverInformation.js Line 13:6: React Hook useEffect has a missing dependency: 'name'. Either include it or remove the dependency array react-hooks/exhaustive-deps Search for the keywords to learn more about each warning. To ignore, add // eslint-disable-next-line to the line before.

      This error tells you that the function in your effect has dependencies that you are not explicitly setting. In this situation, it’s clear that the effect wouldn’t work, but there are times when you may be comparing prop data to stateful data inside the component, which makes it possible to lose track of items in the array.

      The last thing to do is to add some defensive programming to your component. This is a design principle that emphasizes high availability for your application. You want to ensure that your component will render even if the data is not in the correct shape or if you do not get any data at all from an API request.

      As your app is now, the effect will update the riverInformation with any type of data it receives. This will usually be an object, but in cases where it’s not, you can use optional chaining to ensure that you will not throw an error.

      Inside RiverInformation.js, replace the instance of an object dot chaining with optional chaining. To test if it works, remove the default object {} from the useState function:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      import React, { useEffect, useState } from 'react';
      import PropTypes from 'prop-types';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation({ name }) {
        const [riverInformation, setRiverInformation] = useState();
      
        useEffect(() => {
          getRiverInformation(name)
          .then(data =>
            setRiverInformation(data)
          );
        }, [name])
      
        return(
          <div>
            <h2>River Information</h2>
            <ul>
              <li>Continent: {riverInformation?.continent}</li>
              <li>Length: {riverInformation?.length}</li>
              <li>Outflow: {riverInformation?.outflow}</li>
            </ul>
          </div>
        )
      }
      
      RiverInformation.propTypes = {
        name: PropTypes.string.isRequired
      }
      

      Save and close the file. When you do, the file will still load even though the code is referencing properties on undefined instead of an object:

      River Information Updating After Load, 4

      Defensive programming is usually considered a best practice, but it’s especially important on asynchronous functions such as API calls when you can’t guarantee a response.

      In this step, you called asynchronous functions in React. You used the useEffect Hook to fetch information without triggering re-renders and triggered a new update by adding conditions to the useEffect array.

      In the next step, you’ll make some changes to your app so that it updates components only when they are mounted. This will help your app avoid memory leaks.

      Step 2 — Preventing Errors on Unmounted Components

      In this step, you’ll prevent data updates on unmounted components. Since you can never be sure when data will resolve with asynchronous programming, there’s always a risk that the data will resolve after the component has been removed. Updating data on an unmounted component is inefficient and can introduce memory leaks in which your app is using more memory than it needs to.

      By the end of this step, you’ll know how to prevent memory leaks by adding guards in your useEffect Hook to update data only when the component is mounted.

      The current component will always be mounted, so there’s no chance that the code will try and update the component after it is removed from the DOM, but most components aren’t so reliable. They will be added and removed from the page as the user interacts with the application. If a component is removed from a page before the asynchronous function resolves, you can have a memory leak.

      To test out the problem, update App.js to be able to add and remove the river details.

      Open App.js:

      • nano src/components/App/App.js

      Add a button to toggle the river details. Use the useReducer Hook to create a function to toggle the details and a variable to store the toggled state:

      async-tutorial/src/components/App/App.js

      import React, { useReducer, useState } from 'react';
      import './App.css';
      import RiverInformation from '../RiverInformation/RiverInformation';
      
      function App() {
        const [river, setRiver] = useState('nile');
        const [show, toggle] = useReducer(state => !state, true);
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <div><button onClick={toggle}>Toggle Details</button></div>
            <button onClick={() => setRiver('nile')}>Nile</button>
            <button onClick={() => setRiver('amazon')}>Amazon</button>
            <button onClick={() => setRiver('yangtze')}>Yangtze</button>
            <button onClick={() => setRiver('mississippi')}>Mississippi</button>
            {show && <RiverInformation name={river} />}
          </div>
        );
      }
      
      export default App;
      

      Save the file. When you do the browse will reload and you’ll be able to toggle the details.

      Click on a river, then immediately click on the Toggle Details button to hide details. React will generate an error warning that there is a potential memory leak.

      Warning when component is updated after being removed, 5

      To fix the problem you need to either cancel or ignore the asynchronous function inside useEffect. If you are using a library such as RxJS, you can cancel an asynchronous action when the component unmounts by returning a function in your useEffect Hook. In other cases, you’ll need a variable to store the mounted state.

      Open RiverInformation.js:

      • nano src/components/RiverInformation/RiverInformation.js

      Inside the useEffect function, create a variable called mounted and set it to true. Inside the .then callback, use a conditional to set the data if mounted is true:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      
      import React, { useEffect, useState } from 'react';
      import PropTypes from 'prop-types';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation({ name }) {
        const [riverInformation, setRiverInformation] = useState();
      
        useEffect(() => {
          let mounted = true;
          getRiverInformation(name)
          .then(data => {
            if(mounted) {
              setRiverInformation(data)
            }
          });
        }, [name])
      
      
        return(
          <div>
            <h2>River Information</h2>
            <ul>
              <li>Continent: {riverInformation?.continent}</li>
              <li>Length: {riverInformation?.length}</li>
              <li>Outflow: {riverInformation?.outflow}</li>
            </ul>
          </div>
        )
      }
      
      RiverInformation.propTypes = {
        name: PropTypes.string.isRequired
      }
      

      Now that you have the variable, you need to be able to flip it when the component unmounts. With the useEffect Hook, you can return a function that will run when the component unmounts. Return a function that sets mounted to false:

      async-tutorial/src/components/RiverInformation/RiverInformation.js

      
      import React, { useEffect, useState } from 'react';
      import PropTypes from 'prop-types';
      import { getRiverInformation } from '../../services/rivers';
      
      export default function RiverInformation({ name }) {
        const [riverInformation, setRiverInformation] = useState();
      
        useEffect(() => {
          let mounted = true;
          getRiverInformation(name)
          .then(data => {
            if(mounted) {
              setRiverInformation(data)
            }
          });
          return () => {
           mounted = false;
         }
        }, [name])
      
        return(
          <div>
            <h2>River Information</h2>
            <ul>
              <li>Continent: {riverInformation?.continent}</li>
              <li>Length: {riverInformation?.length}</li>
              <li>Outflow: {riverInformation?.outflow}</li>
            </ul>
          </div>
        )
      }
      
      RiverInformation.propTypes = {
        name: PropTypes.string.isRequired
      }
      

      Save the file. When you do, you’ll be able to toggle the details without an error.

      No warning when toggling, 6

      When you unmount, the component useEffect updates the variable. The asynchronous function will still resolve, but it won’t make any changes to unmounted components. This will prevent memory leaks.

      In this step, you made your app update state only when a component is mounted. You updated the useEffect Hook to track if the component is mounted and returned a function to update the value when the component unmounts.

      In the next step, you’ll asynchronously load components to split code into smaller bundles that a user will load as needed.

      Step 3 — Lazy Loading a Component with Suspense and lazy

      In this step, you’ll split your code with React Suspense and lazy. As applications grow, the size of the final build grows with it. Rather than forcing users to download the whole application, you can split the code into smaller chunks. React Suspense and lazy work with webpack and other build systems to split your code into smaller pieces that a user will be able to load on demand. In the future, you will be able to use Suspense to load a variety of data, including API requests.

      By the end of this step, you’ll be able to load components asynchronously, breaking large applications into smaller, more focused chunks.

      So far you’ve only worked with asynchronously loading data, but you can also asynchronously load components. This process, often called code splitting, helps reduce the size of your code bundles so your users don’t have to download the full application if they are only using a portion of it.

      Most of the time, you import code statically, but you can import code dynamically by calling import as a function instead of a statement. The code would be something like this:

      import('my-library')
      .then(library => library.action())
      

      React gives you an additional set of tools called lazy and Suspense. React Suspense will eventually expand to handle data loading, but for now you can use it to load components.

      Open App.js:

      • nano src/components/App/App.js

      Then import lazy and Suspense from react:

      async-tutorial/src/components/App/App.js

      import React, { lazy, Suspense, useReducer, useState } from 'react';
      import './App.css';
      import RiverInformation from '../RiverInformation/RiverInformation';
      
      function App() {
        const [river, setRiver] = useState('nile');
        const [show, toggle] = useReducer(state => !state, true);
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <div><button onClick={toggle}>Toggle Details</button></div>
            <button onClick={() => setRiver('nile')}>Nile</button>
            <button onClick={() => setRiver('amazon')}>Amazon</button>
            <button onClick={() => setRiver('yangtze')}>Yangtze</button>
            <button onClick={() => setRiver('mississippi')}>Mississippi</button>
            {show && <RiverInformation name={river} />}
          </div>
        );
      }
      
      export default App;
      

      lazy and Suspsense have two distinct jobs. You use the lazy function to dynamically import the component and set it to a variable. Suspense is a built-in component you use to display a fallback message while the code is loading.

      Replace import RiverInformation from '../RiverInformation/RiverInformation'; with a call to lazy. Assign the result to a variable called RiverInformation. Then wrap {show && <RiverInformation name={river} />} with the Suspense component and a <div> with a message of Loading Component to the fallback prop:

      async-tutorial/src/components/App/App.js

      import React, { lazy, Suspense, useReducer, useState } from 'react';
      import './App.css';
      const RiverInformation = lazy(() => import('../RiverInformation/RiverInformation'));
      
      function App() {
        const [river, setRiver] = useState('nile');
        const [show, toggle] = useReducer(state => !state, true);
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <div><button onClick={toggle}>Toggle Details</button></div>
            <button onClick={() => setRiver('nile')}>Nile</button>
            <button onClick={() => setRiver('amazon')}>Amazon</button>
            <button onClick={() => setRiver('yangtze')}>Yangtze</button>
            <button onClick={() => setRiver('mississippi')}>Mississippi</button>
            <Suspense fallback={<div>Loading Component</div>}>
              {show && <RiverInformation name={river} />}
            </Suspense>
          </div>
        );
      }
      
      export default App;
      

      Save the file. When you do, reload the page and you’ll find that the component is dynamically loaded. If you want to see the loading message, you can throttle the response in the Chrome web browser.

      Component Loading

      If you navigate to the Network tab in Chrome or Firefox, you’ll find that the code is broken into different chunks.

      Chunks

      Each chunk gets a number by default, but with Create React App combined with webpack, you can set the chunk name by adding a comment by the dynamic import.

      In App.js, add a comment of /* webpackChunkName: "RiverInformation" */ inside the import function:

      async-tutorial/src/components/App/App.js

      import React, { lazy, Suspense, useReducer, useState } from 'react';
      import './App.css';
      const RiverInformation = lazy(() => import(/* webpackChunkName: "RiverInformation" */ '../RiverInformation/RiverInformation'));
      
      function App() {
        const [river, setRiver] = useState('nile');
        const [show, toggle] = useReducer(state => !state, true);
        return (
          <div className="wrapper">
            <h1>World's Longest Rivers</h1>
            <div><button onClick={toggle}>Toggle Details</button></div>
            <button onClick={() => setRiver('nile')}>Nile</button>
            <button onClick={() => setRiver('amazon')}>Amazon</button>
            <button onClick={() => setRiver('yangtze')}>Yangtze</button>
            <button onClick={() => setRiver('mississippi')}>Mississippi</button>
            <Suspense fallback={<div>Loading Component</div>}>
              {show && <RiverInformation name={river} />}
            </Suspense>
          </div>
        );
      }
      
      export default App;
      

      Save and close the file. When you do, the browser will refresh and the RiverInformation chunk will have a unique name.

      River Information Chunk

      In this step, you asynchronously loaded components. You used lazy and Suspense to dynamically import components and to show a loading message while the component loads. You also gave custom names to webpack chunks to improve readability and debugging.

      Conclusion

      Asynchronous functions create efficient user-friendly applications. However, their advantages come with some subtle costs that can evolve into bugs in your program. You now have tools that will let you split large applications into smaller pieces and load asynchronous data while still giving the user a visible application. You can use the knowledge to incorporate API requests and asynchronous data manipulations into your applications creating fast and reliable user experiences.

      If you would like to read more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.



      Source link

      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!



      Source link