How Islands Architecture Can Help to Scale Design

Using AgnosticUI and Astro one can build an application that uses React, Vue, Svelte, or Astro components

Pagoda Illustration
Illustration by Rob Levin

Composability Summit

I will be speaking at The Composability Summit in about a month (more to come soon). The topic is about creating systems of design that scale across teams and UI frameworks, and as I start to prepare for my talk I figured I’d write a blog post to help form my thoughts.

What is a design that scales?

In terms of web applications, a design that scales, is one that allows multiple teams to work in various UI frameworks (React, Vue, Svelte, Astro, etc.) to deliver micro frontends while maintaining consistency of brand.


Imagine a company with multiple teams that all have various developer tastes and preferences in terms of technology stack and proficiency. One team loves React. Another Vue. And yet another Svelte. You get the idea.

You’d like to allow these teams to create separate but cohesive micro frontends that can be assembled together to form a cohesive page or application.

Why?

Well, many reasons—one team may be particularly more productive in a certain framework. Or perhaps they just really enjoy Framework X better then Framework Y. Or, maybe you’re using an outside agency, or an offshore team, and they simply have a ton of experience and comfort in Framework X.

You’re first intuition might be that this would be infeasible as you’d never want to ship multiple framework runtimes to poor bandwidth contrained users, right!? Well, enter Astro (or other choices like Slinkity or marko by eBay). All these provide a means to partially hydrate.

What does it mean to partially hydrate?

Essentially, it means shipping any framework code or runtime that is needed to support a component’s dynamic runtime requirements. Things like state changes and interactivity are prime examples. By utilizing partial hydration, you can selectively choose what gets hydrated client side.

Components that do not opt-in for hydration will instead be rendered at build-time on the server. In fact, even JavaScript-based components written in React, Svelte, Vue, etc., will get served up as HTML by default. This reduces the amount of JavaScript shipped which typically improves your app’s performance. In Astro, this selective hydration is done with client directives that look like the following:

<YourReactThingy client:load />

If you to not explicitly opt-in with the client directive and instead do:

<YourReactThingy />

The component will get converted to HTML on the server at build-time. Read more about how Astro does partial hydration here.

What is Islands Architecture?

In Islands Architecture one mentally maps these client hydrated components to the idea of small islands surrounded by static server-rendered sections. The pioneers of this idea were Katie Sylor-Miller and Jason Miller, and a popular graphic to show this concept follows:

Islands Architecture—Jason Miller
Source: Jason Miller

Looking at this image, you can imagine the sidebar and header apps being built in Svelte; the Advertisement app built in Vue; the Carousel in React; and perhaps the rest of the page in Astro server-built components that get sent down as HTML.

So, besides affording polyglot teams a way to work in their favorite UI framework, better performance is also achieved.

Show me the code!

I’ve built a simple SignUp page prototype using some of these concepts:

Signup Demo screen grab

You should see the white rectangular annotations in the screenshot above. Those indicate the following core components:

  1. Signup Form (Vue)
  2. Language Selector (Svelte)
  3. Newsletter (Astro)

In fact, the newsletter button triggers a dialog, but it still can be thought of as a single component.

Otherwise, we have the illustration hero, a logo, and the general layout. Those can also be build-time assembled using Astro components.

One can imagine a polyglot team using a tracking tool to work on these pieces individually:

Kanban board of agile teams

The code for each of these components is available on GitHub and a bit out of scope for this article. So, I will simply show the main layout Astro page to give a hellicopter view of how these get stitched together.

That said, running the above linked code on GitHub should be as easy as npm i && npm run dev so I’d encourage you to code dive if you feel so compelled!

Astro pages have a top Component Script which can be thought of as frontmatter if you’ve used that before. It’s described here and uses code fences (basically 3 hyphens) to delineate this section:

---
import LanguageSelector from '../components/svelte/LanguageSelector.svelte';
import Signup from '../components/vue/Signup.vue';
import Newsletter from '../components/astro/Newsletter.astro';

let title = 'Astro/AgnosticUI Site';
---

Astro pages also have a component template section:

<html lang="en">
<head>...</head>
<body>
	<main class="layout">
    <section class="left">
      <div class="flex-inline flex-row items-center justify-between">
        <!-- Logo -->
        <img class="logo" width="48" height="48" alt="AgnosticUI Logo" />
        <!-- Language Selector (client hydrated on load Svelte component)-->
        <LanguageSelector client:load />
      </div>
      <Signup client:load />
    </section>
    <section class="right">
      <!-- Hero Illustration -->
      <picture>
        <source type="image/webp" srcset="/assets/pagoda-illo-optimized.webp">
        <source type="image/jpeg" srcset="//assets/pagoda-illo-optimized.jpg" >
        <img class="full-bleed" width="1344" height="1680" alt="Pagoda Illustration" src="/assets/pagoda-illo-optimized.jpg" />
      </picture>
      <!-- Newsletter Button (triggers dialog. Astro) -->
      <Newsletter />
    </section>
	</main>
  <script is:inline>
    // The is:inline in the script tag prevents this script section from being hoisted
    // into the server built bundle.
    const setLogo = (currentMode) => {
      const logoPath = currentMode === "dark" ? "/assets/ag-logo-inverted.svg" : "/assets/ag-logo.svg";
      document.querySelector('.logo').src = logoPath;
    }
    // ...more code omitted for brevity
  </script>
</body>
</html>

The comments should be pretty self explanatory, but the most interesting parts are the <Signup client:load>, the <Newsletter>, and the <LanguageSelector client:load>. Can you guess which of these is NOT client hydrated? Yes, the Newsletter.


Looking at the Signup (Vue) and LanguageSelector (Svelte) components, you can imagine that there’s some state being shared there—the form labels need to reflect the language selected. But, these two are using different UI frameworks. So how can they share state?

Nanostores

At time of writing, Astro doesn’t provide a means to do some sort of inter-framework component communication (and perhaps that’s not really Astro’s job to do!), but we can use nanostores to achieve that.

Essentially, nanostores provides a publish / subscribe system for sharing state between these disparate components. The easiest way to grok how it works is to look at the conveniently simple Astro / Nanostores demo.

Otherwise, here’s the language store which should give you an idea:

import { atom } from 'nanostores';

const initialValue: string = 'en_US';

const language = atom(initialValue);

const setLanguage = function setLanguage(lang: string) {
  language.set(lang)
}

export { language, setLanguage };

And then for brevity, here’s the snippet of code that the language selector runs when a new option is selected:

on:selected={(e) => {
  const selectedLanguage = e.detail;
  if (selectedLanguage) {
    setLanguage(selectedLanguage);
  }
}}

Of course setLanguage was imported (not shown).

Now, the form component can listen for any updates with:

import { useStore } from "@nanostores/vue";
import { language } from "../../store/language";
// When language gets updated, useStore will cause currentLanguage
// to update and that will cause Vue to rerender this view
const currentLanguage = useStore(language);

I’ve kept these snippets relatively small to prevent cognitive overload, but again, I encourage you to peruse and/or run the actual code on GitHub.

Conclusion

I think it’s an exciting time in web development to be able to use multiple UI frameworks on the same page while actually improving performance. This process isn’t automated yet, and so you do have to deal with some things yourself like: which components do I need to hydrate? how will components from different frameworks share state? Etc.

However, you’ve seen that these challenges can indeed be tackled. I should also mention that we could have easily just made both the language selector and form components in Vue (or all Svelte, React, etc.) to remove the need for nanostores altogether. However, the state code would likely look quite similar even within the same framework so not too big of a win there in my opinion.

But again, this is all very doable today!

If you already have a larger organization with several polyglot frontend teams that enjoy working with different UI frameworks, building apps on the Islands Architecture with AgnosticUI and Astro just might be an option worth trying out.