Join me in this week’s post as I take Astro, React and SolidJS to the dance floor together, building a simple application which has components written in different technologies, yet sharing the same state.
Recently there is a lot of buzz over Astro, with a lot of promises of better performance and smooth UI frameworks integration, so I decided to have a look at it and see if these promises hold.
I really like the idea of Astro’s “islands” and what is even more appealing to me is the fact that you can compose different technologies in these islands and have full interaction between them.
So just to make things clearer here are my goals for this one:
- Have an Astro application up and running
- Have a Counter display component made with react
- Have a Counter controller component with “+” and “-” buttons made with solidJS
- They will all be on the same page sharing the same state, so incrementing or decrementing on the Counter controller will affect the Counter display
- These 2 components will reside in “islands” and will hydrate on page load
Some heavy lifting here, so put your weight lifting belts on and let’s start
I start with by creating a new Astro project using the CLI tool
yarn create astro
I’m choosing “just the basics” as my project’s type (nice coloring going on in the terminal, BTW) and I prefer not to use TypeScript at the moment - god knows we have enough challenges ahead, no need to make it worse ;)
And… that’s it, I can launch the site now using yarn dev
and indeed the site pops up in my browser -
See what we got
VSCode does not handle .astro files well, so we need to install a plugin for it. This plugin seems to do the job well enough.
We have our “index.astro” file which holds the main page of the site, we have the “layout.astro” which is the mainAstro component that actually holds the HTML document that has a “slot” to which the content is appended, and we have a “Card.astro” component file.
I’m currently less interested in how astro composes its parts. I might need to dive into it later on, but now that I got my project set, I would like to start integrating frameworks to it, starting with React
Integrating React
Following the docs I’m adding the React integration to my application:
yarn astro add react
Astro then installs the required dependencies and makes some modifications to the Astro config file, declaring that React is now integrated. Now I can create my React CounterDisplay component -
import React from 'react';
const CounterDisplay = () => {
return <div>0</div>;
};
export default CounterDisplay;
And in my index.astro
file I will import my component and use it -
---
import Layout from '../layouts/Layout.astro';
import CounterDisplay from '../components/CounterDisplay'
---
<Layout title="Welcome to Astro.">
<main>
<CounterDisplay />
</main>
</Layout>
(I’ve removed all the other OOTB components from it)
It works :) My page looks like this now (hold your gasps) -
Right, It is time to move to the other SolidJS component which will display the two buttons for incrementing and decrementing the counter
Integrating SolidJS
Same as with the React integration above, I’m using the astro “add” command to generate the integration:
yarn astro add solid
Now that I have the integration ready, let’s write our CounterController component:
import 'solid-js';
const CounterController = () => {
return (
<div>
<button>+</button>
<button>-</button>
</div>
);
};
export default CounterController;
You are probably wondering why I’m importing solid-js
there, but not importing solid-js
will result in a parsing error:
error Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
(If you know of any other solution to bypass please share in the comments below)
And I add it to the index.astro file as I did for the React Component:
---
import Layout from '../layouts/Layout.astro';
import CounterDisplay from '../components/CounterDisplay'
import CounterController from '../components/CounterController'
---
<Layout title="Welcome to Astro.">
<main>
<CounterDisplay />
<CounterController />
</main>
</Layout>
And now my page looks like this -
Yes, it is butt-ugly but we have a page which holds 2 components, each written in a different technology with so little effort.
Still, my page does not do anything interesting. Clicking on the buttons does not do anything to the Counter display. Let’s see how we can use a shared state for that.
Sharing a state
I would really like to use SolidJS’s signals (or store) for that but since I’m about to share a state between 2 different technologies it is advised that I will use the nano-stores.
I will install the nanostores
and the the packages for React and SolidJS
yarn add nanostores @nanostores/react @nanostores/solid
I then create a file called counter.js
under a “stores” directory and put this content in it:
import {atom} from 'nanostores';
export const counter = atom(5);
(you can read about “atoms” here)
Now that we have the store set, let’s first use it in our React component which displays the counter value.
import React from 'react';
import {useStore} from '@nanostores/react';
import {counter} from '../stores/counter';
const CounterDisplay = () => {
const counterValue = useStore(counter);
return <div>{counterValue}</div>;
};
export default CounterDisplay;
Cool - my page now shows “5” as the Counter value.
It's time to make the buttons do what they should. I’m adding the Solid hook for fetching the store and click event handlers to the CouterController component:
import 'solid-js';
import {useStore} from '@nanostores/solid';
import {counter} from '../stores/counter';
const CounterController = () => {
const counterValue = useStore(counter);
return (
<div>
<button
onClick={() => {
counter.set(counterValue() + 1);
}}
>
+
</button>
<button
onClick={() => {
counter.set(counterValue() - 1);
}}
>
-
</button>
</div>
);
};
export default CounterController;
Refreshing the page and… nothing happens when I click. Why?
Well, this is where the “islands” concept comes into play.
Astro’s Islands
Astro prepares a static markup content from the astro files, but once it reaches the browser there is no JS running to make the page’s components interactive. In order to instruct it to hydrate on the client we need to add the “client:...” directives to our components, and tell Astro how we would like to hydrate it, or more accurately - when.
This is a very powerful feature which allows better control over the performance of your page load and JS execution. I will add the instruction to hydrate my components upon page load:
<Layout title="Welcome to Astro.">
<main>
<CounterDisplay client:load/>
<CounterController client:load/>
</main>
</Layout>
And now indeed when I click the buttons the counter acts accordingly.
Hey, we got our app working!
The hooks issue
I can’t be all that good right?
You can see that both my component are declared like this:
const CounterController = () => {
...
};
export default CounterController;
And this format causes Astro to through this error:
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
Following the instruction on this GitHub thread, what you need to do in order to solve it is to export a named function instead, like so:
import React from 'react';
import {useStore} from '@nanostores/react';
import {counter} from '../stores/counter';
export default function CounterDisplay() {
const counterValue = useStore(counter);
return <div>{counterValue}</div>;
}
This, BTW, only happens for React. The SolidJS component does not have this issue.
Wrapping up
So what have we got -
We have an application with 2 components, each built with a different technology but sharing the same state. We can also control when we want them to hydrate when they reach the browser.
By enabling that, Astro gives us the option to slowly migrate from one technology to the other, or even have several UI frameworks co-exist on the same page/app. This is very powerful and an ability FE developers have been needing for a long time.
I’m very curious to see how it will evolve and affect other technologies which have overlapping features out there.
The code discussed in this post can be found in this GitHub repo - https://github.com/mbarzeev/astro-lab
As always if you have any questions or comments, be sure to leave them in the comments section below.
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻