4 min read
Astro, MDX, Islands and State

Background

I wanted to get familiar with client-side state management and Astro-Islands to help with creating content. I like the idea of being able to create small, runnable sets of code I could write about and reference later. To do this I first used Astro’s recommended Nanostores and then moved to XState (which I enjoy for more complex state mgmt).

Nanostores Counter

First I installed Nanostores via npm install nanostores @nanostores/react and created a simple directory structure adjacent to this post index.mdx file:

Directory structure

Then I quickly went about populating my new files.

./nanostores-counter/component.astro

---
import { Counter } from "./Counter";
---
<div>
  <Counter client:load />
</div>

Here, I’m simply importing my Counter.tsx (react), file and giving it the client:load directive so that Astro knows how to handle it.

./nanostores-counter/clickStore.ts

import { atom } from "nanostores";
export const clickCount = atom(0);

I create a clickCount state atom and set its initial value to 0.

./nanostores-counter/Counter.tsx

import { useStore } from "@nanostores/react";
import { clickCount } from "./clickStore";

export const Counter = () => {
  const $clickCount = useStore(clickCount);
  const handleClick = () => clickCount.set($clickCount + 1);

  return (
    <>
      <button onClick={handleClick}>Click Me</button>
      <div># clicks: {$clickCount}</div>
    </>
  );
};

Here I’m making a very basic React counter button and label that adds +1 for each click.

./index.mdx

import NanostoresCounter from "./nanostores-counter/component.astro";

<NanostoresCounter />;

Finally I just import the component.astro into this post annnnd done!


# clicks: 0

XState Counter

I’ve really enjoyed using Xstate and have it in mind for use in a future post, so I want use it to re-implement the above Counter. Once I can do this I feel like I can move on to post about more complex client-side “stuff”. To get up and running I created a new directory adjacent to this post (index.mdx) called xstate-counter and installed xstate via:

npm install xstate @xstate/react

Final Directory Structure

I simply copied component.astro and Counter.tsx into a new directory and wired them into an xstate machine. The component.astro code is exactly the same and is only being used to load my react Counter.tsx component. The Counter.tsx is updated as follows

./xstate-counter/Counter.tsx

import { useActor } from "@xstate/react";
import { countMachine } from "./countMachine";

export const Counter = () => {
  const [state, send] = useActor(countMachine);
  const handleClick = () => send({ type: "INC" });
  return (
    <div>
      <button
        className="p-2 border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 transition-colors duration-300 ease-in-out"
        onClick={handleClick}
      >
        Click Me
      </button>
      <div># clicks: {state.context.count}</div>
    </div>
  );
};

Here you can see that I import useActor and countMachine and a bit more boiler plate to send a {type: "INC"} to the countMachine. Overall this is a bit more boilerplate but still quite simple.

./xstate-counter/countMachine.ts

import { createMachine, assign } from "xstate";

export const countMachine = createMachine({
  context: {
    count: 0,
  },
  on: {
    INC: {
      actions: assign({
        count: ({ context }) => context.count + 1,
      }),
    },
  },
});

This is a fairly naked state-machine with only the INC event handle.

Final ./index.mdx

import NanostoresCounter from "./nanostores-counter/component.astro";

<NanostoresCounter />;

... content ...

import XStateCounter from "./xstate-counter/component.astro";

<XStateCounter/>;

Finally, importing just as before into this file (index.mdx) gives me a similar counter:


# clicks: 0