Resolving modules dynamically with Webpack
One of the great advantage of using Webpack is the ability to load modules asynchronously and on the fly.
This comes in handy when, based on a dynamic property from our client, we want to load a certain module, or even many and combine them. Maybe we would like to overwrite certain properties on each module when we pull it in. Say, for instance that each country has a different set of validatioon functions to validate a name, phone number, etc...
The Problem
Say, for instance, that based on a user's country code we want to merge a certain number of different files. Maybe this country falls under certain legal regulations which can be generalized and has logic which is kept in one file, but the country code itself also has a special set of regulations which takes precedence over certain rules contained in our more generalized file. What we can do is load each module using Webpack and overwrite certain methods on each file based on precedence. We can use the native Array.reduce()
method to overwrite properties on each module (imagining that each file is a collection of object methods).
Here is an example. Imagine for this example that this file renders our main client side component and before doing so loads the requisite properties.
The solution
// load-files.js
const baseFileOne = require('../assets/importantStuffOne/base');
const baseFileTwo = require('../assets/importantStuffTwo/base');
function getBundles(moduleKeys, folder = '', baseBundle) {
const resolvedBundles = Array.isArray(moduleKeys) && moduleKeys.map(key => {
/* we first check to make sure our passed module keys is in fact an array
so we don't cause any exception to be thrown. */
return new Promise((resolve, reject) => {
import(`./${folder}/${key}`)
.then(result => resolve(result))
.catch(err => reject(err));
}).catch(err => { /* bundle not found. ignore and do nothing */ })
});
if (resolvedBundle) { // existence check
return Promise.all(resolvedBundles).then(bundlesArray => {
return bundlesArray.reduce((acc, curr) => ({ ...acc, ...curr}), baseBundle);
})
}
return Promise.resolve(baseBundle);
}
Using our base bundle as an accumulator, we can then add all methods from our legal region or country as applicable. Because of the way reduce iterates and the way we have ordered our array.. we can control and introduce a rule of precedence. All methods from legal region will first be added to baseBundle, replacing any methods that math. Then, legal country will be added, again replacing any methods which have already been added to the baseBundle.
Then, in this same file, we can call our initilize function. This function will call our getBundles function however many times is necessary to load the requisite information. For our purposes, we can call the above function twice, once for our country and region specific validations, and then once again for event handlers.
// load-files.js
function initializeApp(req, payload) {
const country = req.session.countryCode;
/* imagine we somehow find and store our users countryCode based on their IP address */
const legalRequirement = req.session.legalRule && req.session.legalRule.main;
/* double check to make sure legalRule exists before we check for a property on it. */
const moduleKeys = [legalRequirement, country];
return Promise.all([
getBundles(moduleKeys, 'validations'),
getBundles(moduleKeys, 'eventHandlers')
]).then([validations, eventHandlers] => renderApp({ validations, eventHandlers }))
/* we can imagine that our renderApp function renders our actual component and we
pass it the requisite props for validations and eventhandlers here. */
}