Combining ReactJS and VueJS in one website using Astro 🚀
Let's do a quick, very very basic setup of a small application that holds two micro applications: one built in ReactJS and one built in VueJS. The installation of Astro is a breeze, we'll just fire all of the CLI commands sequentially and then dive in, shall we?
First install Astro itself:
npm create astro@latest
This will follow the (cute) installation process and we can stick to defaults. Go to the folder that Astro has suggested for you (nice touch!) and continue with adding the frameworks. We'll add both ReactJS, VueJS and Tailwind for styling, all in individual steps. Again, here too you can stick to the defaults you prefer:
npx astro add react
npx astro add vue
npx astro add tailwind
Now you can open up the project in your favourite IDE to create the two components. For the sake of this tutorial, it'll be simple counters, since that's the universal default I guess! 😅
Adding a ReactJS mini application to Astro
For the sake of quick organisation, I've created a 'react' folder in the 'components' folder and created a file called 'Counter.jsx'. You can recreate it yourself or paste the following contents in the file before saving:
import { useState } from 'react'
function Button(props) {
let { action, title } = props;
return <button onClick={action} className="inline-block rounded bg-blue-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">{title}</button>;
}
function ReactCounter() {
const [count, setCount] = useState(0);
let incrementCount = () => setCount(count + 1);
let decrementCount = () => setCount(count - 1);
let resetCount = () => setCount(0);
return (
<div className="app">
<p>I'm a ReactJS Component ⚛</p>
<p>Count: {count}</p>
<div className="buttons">
<Button title={"-"} action={decrementCount} />
<Button title={"+"} action={incrementCount} />
<Button title={"Reset"} action={resetCount} />
</div>
</div>
);
}
export default ReactCounter;
Note that I'm not a daily React developer so this might not be optimal code. Apologies for any offensive coding techniques I've used!
In the default `index.astro` file, we can now import the component and add it to the template. The imports are denoted by the `---` code fences. Read more about the syntax in the official docs. There are some default imports to help you get orientated if you've selected the optional prefilled template. If not, simply add the import statement between a 'block' of triple dashes:
---
import ReactCounter from "../components/react/Counter.jsx";
---
You can now render the React component using the (familiar) template syntax:
<Layout title="Welcome to Astro.">
<ReactCounter client:only="react />
</Layout>
Again, if you've installed with content examples, there is already some HTML structure defined. Then just replace the content below the `<h1>` element with the React counter.
The `client:only="react` is important here, because Astro by default tries to convert it to server side rendered HTML. In our case, we have some interactive element here, so we want to preserve the interactivity to the client.
If you serve the page, it should work!
Adding a VueJS mini application to Astro
Similar to the 'react' folder, I've created a 'vue' folder and created a 'Counter.vue' file there with the following contents:
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
const decrement = () => count.value--;
const resetCounter = () => count.value = 0;
</script>
<template>
<p>I'm a VueJS component 💚</p>
<p>Result: {{ count }}</p>
<button type="button" @click="decrement" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">-</button>
<button type="button" @click="increment" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">+</button>
<button type="button" @click="resetCounter" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">Reset</button>
</template>
In the `index.astro` file, we can now import this component and add it to the template too. Simply add the import statement between a 'block' of triple dashes:
---
import ReactCounter from "../components/react/Counter.jsx";
import VueCounter from "../components/vue/Counter.vue";
---
You can now render the Vue component by adding a line like we did with the React component:
<VueCounter client:only="vue" />
To have somewhat of a simple layout, you can copy pasta these contents for the `index.astro` file:
---
import Layout from "../layouts/Layout.astro";
import ReactCounter from "../components/react/Counter.jsx";
import VueCounter from "../components/vue/Counter.vue";
---
<Layout title="Welcome to Astro.">
<main>
<h1 class="w-100">Welcome to <span class="text-gradient">Astro</span></h1>
<div class="flex justify-center">
<div class="p-2 w-1/2">
<ReactCounter client:only="react" />
</div>
<div class="p-2 w-1/2">
<VueCounter client:only="vue" />
</div>
</div>
</main>
</Layout>
<style>
main {
margin: auto;
padding: 1.5rem;
max-width: 60ch;
}
h1 {
font-size: 3rem;
font-weight: 800;
margin: 0;
}
.text-gradient {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
</style>
Zero Configuration
Running this code with `npm astro dev` you should see the two counters running side by side in one frontend application, with zero configuration! 🤯 That is a really cool way of working!
I have yet to dive into the performance aspect of Astro, because it appears to be 🔥🔥🔥 out of the box as well. More Astro posts to come for sure! 🚀
Update: sharing state between the applications!
So while I was talking about this in my organisation, I got some questions on sharing state, so I decided to experiment with both micro app controlling the same counter and it turns out to be just as simple!
Let's iterate over the code we already have. We're going to use Nano Stores, since it's an existing minimal state manager compatible with various frameworks (such as ReactJS and VueJS!
Install using the CLI:
npm install nanostores @nanostores/react @nanostores/vue
Alright, first we'll create a teensy store, holding our counter number, stored in `src/stores/counter.ts`:
import { atom } from 'nanostores';
export const countStore = atom(0);
It really doesn't get any smaller than this!
Refactoring the React Component
Okay, let's first refactor the React component to make use of this store and update the value.
First replace the imports to the Nano Store and counter:
import { useStore } from '@nanostores/react'
import { countStore } from '../../stores/counter'
Next we'll add the reference to the counter and change the methods in the ReactCounter function/component:
const $count = useStore(countStore);
let incrementCount = () => countStore.set(countStore.get() + 1)
let decrementCount = () => countStore.set(countStore.get() - 1)
let resetCount = () => countStore.set(0)
Finally we need to update the reference in the template to show the `$count` as the reactive number:
<p>Count: { $count }</p>
The whole file should now look like this:
import { useStore } from '@nanostores/react'
import { countStore } from '../../stores/counter'
function Button(props) {
let { action, title } = props;
return <button onClick={action} className="inline-block rounded bg-blue-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">{title}</button>;
}
function ReactCounter() {
const $count = useStore(countStore);
let incrementCount = () => countStore.set(countStore.get() + 1);
let decrementCount = () => countStore.set(countStore.get() - 1);
let resetCount = () => countStore.set(0);
return (
<div className="app">
<p>I'm a ReactJS Component ⚛</p>
<p>Count: { $count }</p>
<div className="buttons">
<Button title={"-"} action={decrementCount} />
<Button title={"+"} action={incrementCount} />
<Button title={"Reset"} action={resetCount} />
</div>
</div>
);
}
export default ReactCounter;
You should be able to test it: it should work independently of the Vue component, but we'll soon fix that!
Refactoring the Vue Component
For the VueJS component we'll implement the same steps (the code is becoming very similar now 👀). Replace the imports with the store:
import { useStore } from '@nanostores/vue'
import { countStore } from '../../stores/counter'
And change the methods to update the store rather than internal state:
const $count = useStore(countStore);
const increment = () => countStore.set(countStore.get() + 1)
const decrement = () => countStore.set(countStore.get() - 1)
const resetCounter = () => countStore.set(0)
Again, since we've renamed the `count` constant to `$count` we should update it in the template:
<p>Result: {{ $count }}</p>
And the complete file should look like this now:
<script setup>
import { useStore } from '@nanostores/vue'
import { countStore } from '../../stores/counter'
const $count = useStore(countStore);
const increment = () => countStore.set(countStore.get() + 1)
const decrement = () => countStore.set(countStore.get() - 1)
const resetCounter = () => countStore.set(0)
</script>
<template>
<p>I'm a VueJS component 💚</p>
<p>Result: {{ $count }}</p>
<button type="button" @click="decrement" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">-</button>
<button type="button" @click="increment" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">+</button>
<button type="button" @click="resetCounter" class="inline-block rounded bg-green-400 px-6 pt-2.5 pb-2 text-xs font-medium uppercase leading-normal text-white">Reset</button>
</template>
Shared state!
If you now run the application, you should see that both controls and counters reflect the changes made in either application. The nice part here is that using more framework agnostic libraries and frameworks, we can allow multiple environments to co exist and communicate on the same page.
In my mind this is beneficial in terms of teams taking full ownership of their own little domain and still being able to deliver a unified experience to the visitor! 🙌
Now you could argue that those micro applications will probably have their own micro services attached to it, but you can imagine that a product card on an ecommerce platform will want to be able to open a shopping cart to show that a basket addition has been successful. For these sort of small events, this is a perfect way of achieving that!