I've been using SolidJS a lot more lately, and XState has been a goto in my toolbox for years now. Recently, xstate/store
was added and it's awesome. I decided I'd like to stick the two together and noticed there were only react bindings. I wondered if there was a good reason or if it was just waiting for someone like me to help out:
Alright, let's do it!
The plan of attack
The XState contribution guide makes it easy to figure out how to go about putting together this PR:
- Fork and clone the
xstate
repository - Create a new branch. I checked for a branching convention, but there isn't an obvious one so I went with
xstate-store/solid
- Make the changes
- Add and run tests
- Check the types with
yarn typecheck
- Create a changeset with
yarn changeset
- Create a pull request
Fortunately I already had a fork of XState handy because I poke around in there all the time. I've been wanting to make it so stores can be limited to valid states, but damn, I'm not powerful enough to do it without breaking changes (yet). One of these days I guess.
Writing the code
I started by looking at the React bindings. React and Solid are very similar so I expected to be able to use the React bindings to inform decisions around how to design the Solid bindings. They're dead simple:
SolidJS is reactive so there's no need for a counterpart to useSyncExternalStore
, and that entire block can be replaced by updating a signal inside of the store subscription. This is why I've been working with SolidJS a lot more. Even knowing React so well and working with it for so many years, I can't help feeling like its state management tooling is unnecessarily complex and high-friction.
The SolidJS version
useSelectorWithCompare
was an almost direct transfer to SolidJS, except that no useRef
-like logic is necessary. Storing previous
as a mutable variable is good enough. I also copied the defaultCompare
function as–is because it doesn't seem important to generalize it between React and SolidJS at this point:
useSelector
was a bit more interesting. I really like this combination of createEffect
and onCleanup
here; it's super intuitive what it will do in comparison to useEffect
and its opaque return-as-disposal convention.
It's nice that the creation, use, and disposal of the subscription is pretty easy to follow.
Compare the createEffect
logic to the useSyncExternalStore
logic in React:
There's a lot of implicit and hidden behaviour in the React code.
Making sure it builds for development and testing
In order to get building and testing working, a few changes were necessary:
solid-js
needed to be added to thexstate/store
dev and optional peer dependencies- I needed to create a new
packages/xstate-store/solid
directory with apackage.json
to export this baby function into the world solid-testing-library
would be useful, and it's already in use in thexstate/solid
package- Various little bits of configuration needed to be updated to accommodate and use the new code
The easiest way to do most of this was again to follow the convention already used for the React bindings. In this case, I needed to add exports, files, and preconstruct entry points to xstate/store
's package.json
:
Finally I needed to include instructions in babel.config.js
to include any new SolidJS test files in an override which uses babel-preset-solid
. There was already an override for the xstate/solid
package, so I just needed to add my test file to the existing regular expression:
Writing tests
Most package managers these days make it painless to install and use local packages, so when you fork something you can install it like this:
pnpm add ../xstate/packages/xstate/store
This creates a symbolic link which is convenient for picking up changes as you work. When I built xstate
, I could restart my language server in the editor that's using xstate/store
and immediately see my changes. I love how easy this has gotten over the years.
My main goal with the tests was to ensure that a) I could react to specific store changes and ignore others, and b) components rerender exactly as I'd expect and want them to.
I noticed a lot of people check for rerenders in SolidJS using variables scoped outside of components, where they selectively opt to increment them in contexts where they think the component would rerender. This seemed pretty flimsy so I looked for more reliable ways, and I think I found something in createRenderEffect
. I created a little utility to drop into components where you expect (or don't expect) rerenders called useRenderTracker
, and I like it:
Along with this I wrote a few tests to validate useSelector
's behaviour. It went surprisingly smoothly.
Creating a changeset
Creating a changeset was pleasantly easy, and I haven't encountered the tool they're using before. It guides you through choosing which packages should be included in the changeset, if it's a major or minor change, and adding a message describing the changes. This is great, and something I'd like to add to projects in the future.
The pull request
Here's the final PR! Hopefully it's put to good use.
Closing
I think I'll contribute to XState more often. The team is super responsive and kind, the software is amazing, and there's a lot I could learn in there. The more I think of it, the more I realize the library has had such a positive impact on my career.
Maybe I'll get back to that branch with valid store states.
Creating SolidJS bindings for xstate/store
SolidJS and XState are growing libraries with exceptionally happy users, including me. Here's a documentary of me finding more reasons to use them.