Isomorphic or "Universal JavaScript" is a term that has been around for a while. In this article, we'll take a look at the meaning of the term, and see what it means for developers.
Before getting into the discussion regarding how isomorphism can be applied to JavaScript, let's talk about the word 'isomorphic' and 'isomorphism' first. Isomorphism is a mathematical term, and as with most mathematical terms it has a Greek origin: "iso" meaning equal, morphosis meaning "to shape". Simply put, the term isomorphic means that two (or more things) have a similar form or shape.
Based on the above explanation, we can already see that we are looking at potentially two pieces of code that are identical to each other. But we are talking about more than duplicate code / code duplication here.
Think about this for a moment. In what context could two pieces of code be identical to each other? Since we are talking about JavaScript, isomorphic, or universal JavaScript refers to the same piece of JavaScript code that can be executed on the client (browser) or the server (i.e. Node.js). Essentially it is JavaScript code which can be executed equally regardless of the execution environment.
The term isomorphic and universal are used interchangeably. I need to point out that there are various online discussions about how these terms are similar or even different, however, for this article, we will refer to them as being equal, both meaning that the same piece of JavaScript code can be executed both client and server-side.
The exciting idea is that isomorphic JavaScript does not only apply to frontend frameworks - such as Angular - but also vanilla JavaScript. Frameworks do make our jobs easier by automatically offering these isomorphic features; however, we can go ahead and write vanilla JavaScript as well that can be executed both in the client and on the server.
Recently I self-published a book about Progressive Web Applications, in which you can read a discussion about how the same piece of code can be leveraged to generate HTML on the server-side, and then the same code is reused in the client-side, for the service worker itself. Vanilla Isomorphic JavaScript in action, within a PWA context!
Consider the following code, which is using Node.js to render an HTML page (essentially this is custom server-side rendering (SSR) written from scratch):
import { generateCard } from './utils.mjs';
// additional code, removed for brevity
app.get('/', async (req, res) => {
res.write(fs.readFileSync('partials/header.html'));
res.write(fs.readFileSync('partials/info.html'));
res.write(fs.readFileSync('partials/hero.html'));
res.write(fs.readFileSync('partials/articles.html'));
const articles = await (await fetch('http://localhost:3000/api/news')).json();
articles.forEach((article) => {
const card = generateCard(article);
res.write(card);
});
res.write(fs.readFileSync('partials/articles-close.html'));
res.write(fs.readFileSync('partials/footer.html'));
res.end();
});
The above code renders an index
page by streaming all the content of the partial HTML files, plus it makes a query to a REST API to build up article cards (the application in question is a "news" app)
Notice that in the code above we import a function from utils.mjs
, which looks like the following:
export function generateCard(article) {
const today = new Date();
const added = new Date(article.added);
const difference = parseInt((today - added) / (1000 * 3600));
return `
<div class="col-md-4">
<div class="card mb-4 shadow-sm">
<div class="card-body">
<p class="card-text">${article.slug}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="news/${
article.id
}" class="btn btn-sm btn-outline-primary">Read more</a>
<a href="#" class="btn btn-sm btn-outline-secondary read-later">Read later</a>
</div>
<small class="text-muted">${
difference === 1
? `${difference} hour ago`
: `${difference} hours ago`
}</small>
</div>
</div>
</div>
</div>`;
}
The code above is very straight forward. It is mimicking how a template system would look like by utilising the ES6 Template Literal syntax. It takes article data (which is an object) and builds up a card to display some key information about an article.
Now, in this application, we have to stitch together some HTML on the client-side as well. Why? Because we are working on a PWA: we need to make sure that the client can render the same data via a service worker.
But it would be bad if we would have to repeat ourselves and write the same functionality again. The good news is that we can leverage isomorphic JavaScript, we can use the same function in the client as well (actually, inside the service worker), in the following way:
import { generateCard } from './utils.mjs';
// additional code
workbox.routing.registerRoute(
'/',
workbox.streams.strategy([
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL('partials/header.html'),
}),
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL('partials/info.html'),
}),
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL('partials/hero.html'),
}),
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL('partials/articles.html'),
}),
async ({ event, url }) => {
try {
const response = await apiStrategy.makeRequest({
event,
request: apiRoute,
});
const articles = await response.json();
let cards = '';
articles.forEach((article) => {
cards += generateCard(article);
});
return cards;
} catch (error) {
console.error(error);
}
},
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL(
'partials/articles-close.html'
),
}),
() =>
cacheStrategy.makeRequest({
request: workbox.precaching.getCacheKeyForURL('partials/footer.html'),
}),
])
);
Note that in this code above, we are importing the generateCard
function from utils.mjs
- the same piece of code that we have seen before. But this time, we are using Workbox to build the same index
file, based on entries in the cache, plus by making an API request to data stored, again, in the cache.
And that's the essence of it. The key takeaway is to make sure that we create code that can be consumed by the browser and the server as well.
There are some obvious limitations to isomorphic JavaScript - we can't use the window object and other items for obvious reasons. Therefore if we'd like to use JavaScript on both the client and the server, we cannot rely on packages/objects that are specific to a given environment.
There are a lot of use cases for isomorphic JavaScript, it's certainly an exciting option to explore for projects, and I encourage you to do so. In this article, we had a look at a Progressive Web Application created by vanilla JavaScript, leveraging JavaScript code that can run both in the client and on a server as well.