TypeScript Dynamic module loading + Code splitting with PARCEL [Example App]

Fateh Mohamed 🐢 - Nov 28 '20 - - Dev Community

Today i will not talk much about dynamic import which is available since TypeScript 2.4 and its officially part of ES2020 but i will demonstrate dynamic loading on demand in a small TypeScript application in combination with parcel's code splitting feature. The goal behind is to improve performance specially in big size applications.

we have all used the static imports in ES6, placed on top of files,



// Static import
import * as feature from "./module"; // static import


Enter fullscreen mode Exit fullscreen mode

which is amazing but what about loading modules on demand?



// Dynamic import
import('./feature') 
  .then((module)=> {
    module.method1();
    module.method2();
});


Enter fullscreen mode Exit fullscreen mode

May be we want to load a module on click or navigation event or based on a condition... what if we don't want to load all our modules on startup and convert them to lazy loaded modules so our app will load FASTER 🚀.
i'm Boring right? 😅 lets move on to our use case, to Code 👨‍💻.

Application (Demo)

i've created a TypeScript App ** Country Search** that containq two features:

  1. Search countries by spoken language (EN, ES, AR ...)
  2. Search countries by used currency (USD, EUR, DZD ...)

Consuming a free api and manipulating responses as Observables because i like RXJS 😎...who cares!

So the idea behind is simple; I will not load both modules (Language Search && Currency search) on startup, instead of that modules will be loaded on section selection.
Alt Text

INDEX.TS



// get currency and language buttons elements

const btnCurrency = document.getElementsByClassName("currency-btn")[0] as HTMLElement;
const btnLang = document.getElementsByClassName("lang-btn")[0] as HTMLElement;

// set current section
let selectedSection = "language";


Enter fullscreen mode Exit fullscreen mode

In few words i switch current section based on button click event. Then i will listen to search input changes and based on the selected section (language || currency) i load the correspondent module
The default section is Language with the value 'EN'



fromEvent(
  document.getElementById("search") as FromEventTarget<Event>,
  "input"
).pipe(
    pluck("target", "value"),
    startWith("en"),
    filter((val) => {
      return (val as string).trim().length > 1;
    }),
    distinctUntilChanged(),
    debounceTime(600),
    switchMap((val) =>
      iif(
        () => selectedSection === "language",
        from(import("./language")),
        from(import("./currency"))
      ).pipe(
        switchMap((m) => {
          const getCountries = m.default;
          return getCountries(val as string).pipe(
            catchError((error) => {
              console.log("Caught search error the right way!");
              return of([]);
            })
          );
        })
      )
    )
  )
  .subscribe(console.log);



Enter fullscreen mode Exit fullscreen mode

The interesting part is the iif rxjs operator block where i return one of the two Observables from(import("./language")) or from(import("./currency")) base on the selected section and then get and call the default exported function in that module (getCountries).

That way the currency module is never loaded until the user select that section. here is how does the currency module look like; the language one is pretty much the same.

it contains these steps in order:

  1. Instantiation of CountryService (http calls)
  2. Get the data
  3. Build the html cards

CURRENCY.TS Lazy Loaded



import { tap } from "rxjs/operators";
import { Country } from "~index";
import { CountryService } from "~service/country.service";

console.log("CURRENCY module is lazy loaded....");

const service = new CountryService();

export default (val: string) => {
  const searchMessage = document.getElementById("searchTitle") as HTMLElement;
  const container = document.getElementsByClassName("country-container")[0];
  container!.innerHTML = "<div></div>";
  searchMessage.textContent = "🧐 Searching ...";
  return service.getCountriesByCurrency(val).pipe(
    tap((data) => {
      if (data.length) {
        const list = data.reduce((result: string, item: Country) => {
          result += `<div class="country-item"><img class="flag" src="${item.flag}"  alt="flag"/><div class="description"><h2> ${item?.name} </h2>  <small> ${item?.region}</small> <h2>Capital: ${item?.capital}</h2></div></div>`;
          return result;
        }, "");
        container!.innerHTML = list;
        searchMessage.textContent = `${data.length} ${
          data.length === 1 ? "Country" : "Countries"
        } using ${getFullCurrencyName(data[0]?.currencies, val)}`;
      } else {
        searchMessage.textContent = `No Countries found ☹️`;
      }
    })
  );
};

function getFullCurrencyName(currencies: any[], code: string) {
  const currency = currencies.find((e) => e.code === code.toUpperCase());
  return currency ? currency.name : code;
}




Enter fullscreen mode Exit fullscreen mode

As I said before, i've combined that with Parceljs code splitting feature. A chunk is created for each module and each one can be loaded on demand instead of loading all modules at once when the app starts.

Alt Text

Demo Deployed on Netlify

Repo Github

. . . . . . . .