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
which is amazing but what about loading modules on demand?
// Dynamic import
import('./feature')
.then((module)=> {
module.method1();
module.method2();
});
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:
- Search countries by spoken language (EN, ES, AR ...)
- 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.
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";
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);
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:
- Instantiation of CountryService (http calls)
- Get the data
- 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;
}
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.