Image for post
Image for post
Photo by vaun0815 on Unsplash

There is a “” difference, or better, a limitation, in native import, compared to require: it’s not possible to deal with the module system internal cache, hence it’s not possible to invalidate such cache, or tell that module-x is actually referring to file://x.js, unless lands officially in core too.

” and you’re obviously right, although we don’t always control how is bootstrapped, as example via serverless services, or server side workers, so I’ve decided to explore a different approach.

A Home Made Registry

The technique I am going to show is not suitable for all scenarios, it’s not a substitute of regular import modules, and most importantly, together.

export default new Map;

As simple as it looks, the file should expose a that either client or server side can consume and share, thanks to ’s internal cache.

This is in charge of setting arbitrary module names, so that libraries that could work with both client or server modules, can use such registry instead of importing the libraries themselves.

Check this µland example out:

import registry from './registry.js';const {Component, html, useState} = registry.get('@uland');export default Component(initialState => {
const [count, setCount] = useState(initialState);
return html`
<button onclick=${() => setCount(count + 1)}>
Count: ${count}
</button>`;
});

The Counter component exported by above file, retrieve its utilities via the previously mentioned , which once again, should not be bundled within the rest of the code.

The Client Side

So, how can we define that @uland entry in the registry?

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
// bootstrap registry and libraries
import registry from './registry.js';
import * as uland from '
https://cdn.skypack.dev/uland';
registry.set('@uland', uland);
</script>
<!-- it is important this is a separate script/file -->
<script type="module">
import {render, html} from 'https://cdn.skypack.dev/uland';
import Counter from './counter.js';
render(document.body, () => html`
<div>
A bounce of counters.<hr>
${Counter(0)} ${Counter(1)}
</div>
`);
</script>
</head>
</html>

The reason we need to separate scripts, is that ESM is solved all at once, so that importing the within the same script, where the defines libraries, will parse such before any code is executed, hence once it’s executed, the won’t have the library in yet, as that happens after.

It’s also worth mentioning that this technique coincidentally allows bundling all 3rd party libraries dependencies eventually at once, as long as these are then set as registry entry, so that the bundle could be split in libraries, and the rest of the code could land on demand, via dynamic import, or as separate bundle, still being sure that the is not bundled within the process.

The Server Side

As we don’t have to deal with <script> tags on the server, the only way to grant that the registry works as expected, is to define right away all libraries and dependencies , and import modules that are based on such asynchronously.

// bootstrap registry and libraries
import registry from './registry.js';
import * as uland from 'uland-ssr';
registry.set('@uland', uland);
const {render, html} = uland;// it is important anything using the `registry`
// is dynamically, asynchronously, imported
const {default: Counter} = await import('./counter.js');
render(console.log, () => html`
<div>
A bounce of counters.<hr>
${Counter(0)} ${Counter(1)}
</div>
`);

In this example, the @ulandmodule is provided instead by µland-ssr, so that our component doesn’t need a single line change between client and server.

… but why …

I have different modules that work seamlessly in both client and server, and being able to reuse exact same code across these two different environments, and libraries targets, is something I’ve been doing, in a way or another, pretty much forever, but the ability to “” is something still very hard to do, but I’d love to experiment this approach more.

µhtml and µhtml-ssr are just yet another example where this technique could make development pretty sweet, but the list of client/server modules that expose the same API, but produce dedicated results per environment, is bigger than what’s in my repositories, so I hope this technique will help other developers that, like me, would like to blur the line between client and server, as opposite of writing thing.client.js and thing.server.js, especially when thing.js would be identical for both scenarios 👋

P.S. I can’t wait for importmap to land in NodeJS and make this technique less useful, or needed at all, in the long term!

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store