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
- ./nanostores-counter/
- index.mdx
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!
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
- ./nanostores-counter/
- ./xstate-counter/
- component.astro
- countMachine.ts
- Counter.tsx
- index.mdx
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: