Astro & AgnosticUI Experimental Starter Template

Yet another Astro starter template

Astro + AgnosticUI
Astro + AgnosticUI

Tinkering with Astro

So I was on The Function Call Podcast telling the whole AgnosticUI story and Austin Gil mentioned I might consider looking into rendering multiple component frameworks like React, Vue, and Svelte using Astro. This is an interesting proposition because Astro allows you to control whether JavaScript components get rendered on the server via SSR or on the client via client directives and partial hydration. This potentially means you could have a Svelte or Vue component like an Advertisement widget that SSR and doesn’t need to ship any client JavaScript.

As AgnosticUI supports React, Vue 3, Svelte, and Angular, you can imagine how this starts to open up polyglot microfrontend possibilities!

I had to see for myself and did some experiments this weekend which resulted in this Astro + AgnosticUI Starter. It’s not much more then the Astro starter template but does render React, Vue 3, and Svelte components with AgnosticUI.


The starter template GitHub has the code, but here’s the “gist of it”…

  • Astro provides framework integrations that allow you to use React, Vue 3, Svelte, and a few other UI frameworks.

Note: I found the most success examining the README’s in their stackblitz examples: Vue, React, and Svelte to get these working (although you’ll have my starter to reference as well).

  • Corresponding AgnosticUI packages were installed with npm i agnostic-react agnostic-svelte agnostic-vue
  • The main index.astro page imports the framework components in its frontmatter section:
---
import ReactCounter from '../components/ReactCounter.jsx';
import SvelteCounter from '../components/SvelteCounter.svelte';
import VueCounter from '../components/VueCounter.vue';
----

And then renders them in the template section:

<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width">
	<title>{title}</title>
</head>
<body>
  ...
  <ReactCounter client:visible />
  <SvelteCounter client:visible />
  <VueCounter client:visible />
  ...

Notice the renderer integration doesn’t start importing the component until it enters the viewport and client hydration is done once the component import completes. In reality, you’ll want to combine SSR components with client hydrated ones based on the specifics of your use case. Their client-side hydration directives are listed here.

Here are the framework components themselves. You’ll note we’ve sprinked AgnosticUI into all of them…

React

import { useState } from 'react';
import "agnostic-react/dist/common.min.css";
import "agnostic-react/dist/esm/index.css";
import { Alert, Button } from "agnostic-react";

export default function ReactCounter() {
  const [count, setCount] = useState(0);
  const add = () => setCount((i) => i + 1);
  const subtract = () => setCount((i) => i - 1);

  return (
    <>
      <Alert type="info"><span className="mie12">🔥</span>AgnosticUI — React</Alert>
      <div id="react" className="counter">
        <Button mode="secondary" isRounded onClick={subtract}>-</Button>
        <pre>{count}</pre>
        <Button mode="primary" isRounded onClick={add}>+</Button>
      </div>
    </>
  );
}

Svelte

<script>
  import 'agnostic-svelte/css/common.min.css';
  import { Alert, Button } from 'agnostic-svelte';

  let count = 0;

  function add() {
    count += 1;
  }

  function subtract() {
    count -= 1;
  }
</script>

<Alert type="info"><span class="mie12">🙌</span>AgnosticUI — Svelte</Alert>
<div id="svelte" class="counter">
  <Button mode="secondary" isRounded on:click={subtract}>-</Button>
  <pre>{ count }</pre>
  <Button mode="primary" isRounded on:click={add}>+</Button>
</div>

Vue 3

<template>
  <Alert type="info"><span class="mie12">👌🏽</span>AgnosticUI — Vue 3</Alert>
  <div id="vue" class="counter">
    <Button mode="secondary" isRounded @click="subtract()">-</Button>
    <pre>{{ count }}</pre>
    <Button mode="primary" isRounded @click="add()">+</Button>
  </div>
</template>
<script setup>
import { ref, onMounted, useCssModule } from "vue";
import "agnostic-vue/dist/common.properties.min.css";
import "agnostic-vue/dist/common.resets.min.css";
import "agnostic-vue/dist/common.utilities.min.css";
import "agnostic-vue/dist/opinions.min.css";
// Components CSS
import "agnostic-vue/dist/index.css";
import {
  Alert,
  Button,
} from "agnostic-vue";

const count = ref(0);
const add = () => (count.value = count.value + 1);
const subtract = () => (count.value = count.value - 1);
</script>

Gotchas

So probably the trickiest part of all this is figuring out what to hydrate on the client or SSR. In this example I just went with client side hydration which means we’re shipping all the frameworks and er’thing! We definitely wouldn’t want to that in a production application. Most likely we’d think through if maybe one framework could be used for anything that’s going to require client-side rendering logic and then utilize SSR for the others. You’d be surprised how quickly the need to client hydrate arises — even simple counter examples require client hydration to work properly. So again, setting this up in production will take some forthought but the potential being able to utilize several frameworks on the same page is amazing.

Another example of something that requires client hydration is using Portals. In Vue, we have the builtin Teleport component that helps achieve this and it’s a known that trying to SSR this component is problemattic. As such, they advise to conditionally render the Teleport on mount.


So yeah, you’ll have to do some experimentation to determine how to best leverage Astro’s multiple UI framework possibilities and Island architecture. Probably also worth keeping an eye on Slinkity which apparently can transform 11ty to use partial hydration as well.

My thought is you could use this for interesting things like and Advertisement widget that’s fully SSR, static sections, and fully separated applications (e.g. an Admin, back-office, or marketing site). Lot’s of possibilities and I’m excited to see where all these new approaches land us!