DEV Community

Cover image for Astro, React and SolidJS Dancing Together
Matti Bar-Zeev
Matti Bar-Zeev

Posted on

Astro, React and SolidJS Dancing Together

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 -

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

(I’ve removed all the other OOTB components from it)

It works :) My page looks like this now (hold your gasps) -

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

(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>
Enter fullscreen mode Exit fullscreen mode

And now my page looks like this -

Image description

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);
Enter fullscreen mode Exit fullscreen mode

(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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

And now indeed when I click the buttons the counter acts accordingly.

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 🍻

Photo by Yomex Owo on Unsplash

Top comments (2)

Collapse
 
lirantal profile image
Liran Tal

Just read through it. Nice write-up!
The moment you went through both Solid and React I thought for a second this might be an interesting idea for microfrontends :-)

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Yep, I think that this is the great intention behind the islands.