Vue is the superior framework for Astro
Sep 23, 2024
---
// Wrapper.astro
---
<Layout>
<WordBar client:visible />
<!-- How do you handle global states between these two components -->
<CodeEditor client:visible />
</Layout>
Pre-introduction
Astro is intented to be content-driven static site generator. With “static” in mind, you might think that it is not possible to have interactive or dynamic components, but you actually can!
Astro utilizes the concept of “islands” where you can have different parts of your site to be rendered differently. This means you can have a static part of your site that loads instantly and defers the heavy, interactive UI to be loaded later.
However, the challenge comes when you want to share states between these islands, because
each of them is rendered in different contexts. Usually, for a regular React app,
you’d wrap the whole app with Context
or use a state management library like Redux.
Can we do better?
Introduction
In the making of CONST!, one of the challenges I faced was checking if the paragraph contains the mandated vocabulary, and display the progress of the game.
WordBar
needs to display the set of words and these can be swapped out to get a new word.CodeEditor
should know the word list and notifyWordBar
if a word is found in the paragraph.
For Vue, we have different ways to achieve this.
- Use a parent component to manage the state and pass it down to the children. (We would lose the benefits of Astro islands and have to manage props and emits)
- Pinia but it’s a bit overkill for this use case.
- nanostores which is what recommended by Astro themselves. It is very lightweight but its API is not as convenient for my use case when I want to modify an array.
Using Vue outside of Vue components
I have been working with Vue 3 since 2020, but I never knew we can already create ref
and other reactive objects outside of Vue components.
Yes, we can. You can export it from one file and import it in another Vue component.
export const words = ref([]);
This means you can do this in one file:
<script lang="ts" setup>
import { words } from "@/store/words"; // Import from another file
</script>
<template>
<ul>
<li
v-for="(word, idx) in words"
:key="word.id"
:class="{ 'badge-primary': word.match }"
@click="swapOutWord(idx)"
>
{{ word.name }}
</li>
</ul>
</template>
and do this in another:
<script lang="ts" setup>
import { words } from "@/store/words"; // Import from another file
const code = ref("I ate a cake, and a banana ate me.");
const matchedAll = computed(
() =>
wordStore.value.length > 0 && wordStore.value.every((word) => word.match),
);
watch(
() => code.value,
() => {
wordStore.value = wordStore.value.map((word) => ({
...word,
match: code.value.includes(word.name),
}));
},
{
immediate: true,
},
);
</script>
<template>
<textarea
:class="{
'bg-green-200': matchedAll,
}"
v-model="code"
></textarea>
</template>
But hey, how do you initialize the ref with prefetched data?
If you insert as a prop and the initialize it with onMounted
,
there is a chance that CodeEditor
will be rendered before the words
is
properly initialized. Moreover, to ensure the race condition of CodeEditor
’s
watch
, WordBar
needs to be rendered first.
First, I made WordBar
render first by setting it as client:load
and everything else as client:visible
.
---
const data = [
/* ... */
];
---
<Layout>
<WordBar client:load data={data} />
<CodeEditor client:visible />
</Layout>
Next, what I did was slightly illegal but essentially, I just set the value of the ref directly without any hooks.
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { words } from "@/store/words";
const $props = defineProps<{
data: string[];
}>();
words.value = $props.data.map((name) => ({ name, match: false }));
</script>
And that’s it! Now, try changing the code, swapping out the words, and see the magic happen.
- cake
- sandwich
- apple
- banana
- pizza
What about other frameworks?
- React:
useState
must only be called in a component. - Svelte: While you can create
writable
stores outside of components, it is not usable anywhere else other than Svelte components. - Angular: I have no idea.
- Qwik:
useSignal
must only be called in$component
but this is understandable because Qwik doesn’t “hydrate” - Solid: Yes! You absolutely can!
import { setTest, test } from "./hooks";
export function SolidComponent() {
return (
<div>
<button onClick={increment}>Increment</button>
<p>{test()}</p>
</div>
);
}
0
However, I’m not a fan of having to import both get and set functions in the same file. It might be convenient to do that in Solid, and it also imposes a rule to not accidentally mutate the state directly.
And I can also do that in Vue
<script setup lang="ts">
import { testVue, increment } from "./hooks";
</script>
<template>
<div class="flex items-center justify-center gap-4">
<button class="btn" @click="increment" }>Increment</button>
<p class="grid h-12 w-12 place-content-center rounded bg-base-200">
{{ testVue }}
</p>
</div>
</template>
0
Wait, both numbers are in sync!
If you noticed that both components in Vue and Solid are in sync, 100 points to you! Yes, they are in sync because I made them in sync.
Ihis is how I glued both Solid and Vue together, which I know, looks sacrilegious.
export const [test, setTest] = createSignal(0);
export const testVue = ref(0);
export function increment() {
testVue.value++;
}
watch(
() => testVue.value,
(val) => {
setTest(val);
},
);
createEffect
is frowned upon in Solid because it will never be disposed.
But in this case, Vue doesn’t warn anything about watch
. (I’m not sure. It might
leak memory as I speak but it doesn’t warn.)
Just like how you can store.subscribe
in Svelte stores outside of Svelte components!
Doing it legally.
If you feel that what we did was illegal, you can use createGlobalState
from @vueuse/core
to create a global state.
import { createGlobalState } from "@vueuse/core";
import { ref } from "vue";
export const useWords = createGlobalState(() => {
const words = ref([]);
return { words };
});
Or you can also swap out ref
with useLocalStorage
from @vueuse/core
to persist the state.
Conclusion
Vue is a very flexible framework that allows you to do a lot of things.
- Creating reactive objects outside of Vue components
- Watching the reactive objects outside of Vue components → which means you can
even do it under
<script>
tags in Astro component! - Many helpers in
@vueuse/core
to make your life easier
And this allows you to do a lot of things that you can’t do in other frameworks, especially when you want to share states between islands in Astro.