Building Minesweeper with @xstate/store
@xstate/store is my new favourite lightweight state management tool. It integrates perfectly with all kinds of applications, backend or frontend, including those using React or SolidJS. Here's a bird's eye view of implementing minesweeper with @state/store, SolidJS, and ts-pattern.
State management is hard. If I had to guess, maybe most bugs I work on are some form of errant, brain-derived state management issues. This is an account of trying to find ways to make it less hard, hopefully use my brain less so it can't get in the way, and have some fun in the process.
Why @xstate/store?
At first glance I thought "Why wouldn't I use x, y, or z state management library", but then something about the tiny API and event-driven nature sucked me in. After testing it out on some smaller problems I started to think there was definitely something worthwhile here. On top of being an excellent little store on its own, it integrates seamlessly with XState, and I love XState.
I'm pretty sure by the end of this you'll see the merits as well. This is a great library with a lot of potential.
Why minesweeper?
I implemented it with state machines and the actor model a while back, and it was grotesque. It worked (really well) but it was way too much ceremony for a relatively simple game. I'm sure some people golf this in like 5 LOC.
I wanted to see how safe and robust I could get the game with a much simpler form of state management.
This isn't a complete guide, but it'll give you a sense of how to build the game with @xstate/store
and these tools, and an idea of what it's like to work with the libraries.
Project Outline
My goal from the outset was to essentially create a data structure with associated operations that can only perform valid updates. This can't be guaranteed at the type level here (transitions will let you return any possible state you want), but I found I could make it work really well regardless.
With some loose ideas of how to do that (only operate on known data types, only return states I know are valid, etc) I just needed to implement the game with that data and the events.
Before writing code, I like to have some some kind of a spec. I'm roughly going off of how the classic game works, but I'm sure there are plenty of variations:
- Set up
- The grid can be configured with a 2 dimensional size and number of mines
- The player gets as many flags as the grid has mines when the game starts
- Controls
- Left-clicking reveals a cell, context-clicking sets a flag
- The player can click the face the reset the game at any time
- Chording is a thing, but I don't want to implement it
- Implicit Behaviours
- The game is started when a cell is revealed or a flag is set
- A timer begins as soon as the game is started
- The game stops as soon as the player wins or loses
- When a cell is revealed with no adjacent mines, neighbouring cells can also be revealed.
- Rules
- If a cell is revealed to be safe, it displays the number of mines directly adjacent to it
- If there are no adjacent mines, the adjacent tiles are recursively revealed using the same rules.
- If the player reveals all cells without mines, they win
- If the timer runs out, the player loses
- If a mine cell is revealed, the player loses
- You can never set more than (width * height - 1) mines; we need at least one empty spot on the grid
And that's enough to describe our context and events!
Data and types
I took a data-first rather than type-first approach here. By defining the data structures that make sense for the game I could then derive or infer types from that point on, and they'd always be correct (in theory).
This is great for validation and pattern matching in a type-safe way where the source of truth for types is aligned with things actually happening at runtime. Basically you can't really mess up your types by trying to be clever; they're derived from the data. You don't need to be clever at all, and that's usually ideal.
For now I'm using ts-pattern
to help with this, though lately I've been using Effect and I've come to prefer it. In this case it's a bit too much overhead for what we're trying to achieve, and ts-pattern
's simpler API gives us exactly what we need, so it's still great.
ts-pattern
does better, but also offers runtime validation and data constructors in a package that integrates perfectly with the rest of the Effect API. I think I'll write a bit about this soon.With that settled, here are the initial definitions of the game's data:
These shapes are enough to infer types from and then pattern match in downstream code in order to ensure I’m always working with the type of data I expect to be. Here's how to derive the types:
Events
I could define events as data first as well, but I don’t expect to need to validate or pattern match on event data, and the store itself provides solid type safety here already. A type or interface should be fine for this step.
The EmittedEvent
type isn't strictly required for @xstate/store
, but it's a neat feature in version 2.4 that I wanted to experiment with. In your store—unless it's useful to you—you can skip over it:
All together we can now derive the store's type:
This type can be used to tighten all kinds of logic pertaining the the store now. You could make all of your event handlers external functions you assign to the store so you can test them in isolation, or get the store's snapshot type in order to allow you to write isolated functions which safely operate on your state. It's pretty useful.
Defining the store
My goal here was to be as minimal as I can be without making it needlessly awkward or compromising safety. In this case I think it's a good size of store and few enough events that we can reason about the game easily. I like it. Put together, the store structure looks like this:
Nice! This is a solid foundation. These are all of the possible handlers needed for the game to work.
Now I just need to make sure each event manages state safely and the UI calls the events appropriately. How hard can it be?
A cool thing about this store is that you can now test all of the synchronous state manipulations with unit tests immediately. It's an awesome pattern for TDD. I personally don't do it until late in my process but if that's your thing, this is a nice way to work.
You can see some examples of unit testing the store here. I'm pretty happy with how easy it was. These tests are a great canary for when I'm breaking stuff, though I prefer integration tests in general.
Done! Kind of.
Well, except for the transition logic. Otherwise this is all your store will be in your code base apart from the odd store.send(event)
call. Isn't that crazy? It reminds me a lot of XState. It does such a good job staying out of the way. This is such an invaluable aspect of good software.
Implementation and runtime safety
I'm not going to go too far into how to build minesweeper (it has been done thousands of times in at least half as many ways), but I'll cover a few parts I like where we can leverage our data structures, types, and the store to get pretty solid safety in such a small package.
I decided to use SolidJS for the UI here, but you could use anything. Even vanilla JS would be fine, but I wanted to test useSelector
from @xstate/store/solid
(see a post about that here).
An unrelated obstacle: SolidJS isn't React
Right out of the gate I discovered SolidJS doesn't support a pattern I love, which is using match
from ts-pattern
to determine which UI components I want to render according to which data structure I'm matching on.
In React land this works really well, but for not-entirely-great reasons: it reruns your component's function on every render. This means your match
is called each time your state changes (great) but it also means everything else about the component is run again as well (not great). It's a double edged sword I guess.
In SolidJS land, a component's function runs exactly once. Your match
is run once and you're stuck with what it matched on even as signals within the component update. There are ways around this, but as far as I was able to determine, they break conventions and ultimately you lose out on fine-grained reactivity. A significant point of this endeavour was to get fine-grained reactivity from this store, so... I went with the SolidJS primitives Switch
and Match
. They're good, but not exhaustive. I'll find a way around it eventually.
Getting the data you want
Once you're outside of the store in UI land (or where ever you happen to be), it's pretty easy to pull data from the store. I like to use selectors out of habit (unless I'm directly accessing primitive data types), but you can either do that or pull from the snapshot.
Here's an example based on the GameInfo
component (where the flags, little face, and timer go in a typical minesweeper game):
Rendering exactly what you meant to
After defining all of these cell data structures and getting a discriminated union out of them, you can use it to ensure you’re always rendering the right component (cells in my case). As mentioned, in React you can do this exhaustively (which is so nice), but in SolidJS you can still get a fairly ergonomic and safe solution.
By using isMatching
from ts-pattern
you can be certain it’s matching on the data properly, even though you can't use match
:
Now all I needed to do in order to render these cells and get the fine grained reactivity I wanted is to iterate over them with SolidJS's Index
component:
Nice. So, that'll just dump out a big flat list of cells. That won’t work for actually playing, but it’s not hard to fix with a bit of tailwind and inline css:
In the implementation of the store logic I'm using math to treat the list like a grid. It's possible to use a 2D array here with nested Index
components, but I wasn't able to get the reactivity or tiny DOM updates I knew were otherwise possible so it didn't seem worthwhile.
A convention for handling event data precisely
Another way in which the data structures and pattern matching help out is that they not only define data, but to a degree, intent. This is great in logic where we might reason about objects like real things, and treat them as such, but where the underlying implementation of that data could change.
Take for example when I want to reveal a cell. I’ve got options, but I think the two most obvious ones are these:
- Determine at the UI layer which kind of cell I’m revealing and send the corresponding event to the store.
- Send a single event where the store can figure out what to do based on the event data.
The issue with 1 is that I really don't want my UI to know much about the store. I want the UI to be really dumb, and to interact with the smallest API possible where the implementation of that API is as irrelevant as possible.
The issue with 2 is that I then need much better safety in my event because I’m working with the least data and the most responsibility possible. The good news is that with the approach I’m using, it's trivial to mitigate that concern.
For example, when handling a revealCell
event, I only get an index to look up the cell with. That's enough though, because I can fetch the cell and check the only two conditions in whichI’d need to react, then do the appropriate thing in response:
The alternative to this might be some fragile logic like this:
This might look okay on the surface, and even familiar or comfortable to a lot of us, but there's a major issue here. If the implementation of a cell changes at all, these conditions could fail.
I don't want to check against flags explicitly. I want to check against types in a much more complete sense. This is where definitions of the possible cell states become an expression not only of state but intent; how I intend the logic to handle states, how I intend the game to be played, and so on. This means I can modify the underlying data structures and leave the match(cell)
code exactly as is, because it'll still be matching on the same types of cells - even if property names or potential property values change.
This kind of idiomatic code has been popular in the object oriented world for decades, but that’s often riddled with all kinds of associated complexity and implicitness. In this case, we’re working with plain JavaScript objects. There’s nothing waiting to surprise us here.
Something I also appreciate is that an if
statement only expresses the intent to check arbitrary conditions. A switch
statement is better, but still doesn't protect us against the underlying implementation shifting. With a match
, we can see at the top level exactly what's being matched, what the output can be, if it’s checking all possible conditions, and whether or not it's guaranteed to return a value. That's awesome.
Connecting events to the UI
It’s very straight forward to connect the UI with the store. In the case of the cells, the most interactive component is the CoveredCell
. It leads to all others forms of cells, and apart from the flagged cell, it's the only one with event handlers. It’s a great example of how to hook up events:
Once you load the page, your button (or whatever you created) will be talking to your store and the state changes will be propagating to your components.
Put it all together and...
It works really well!
By combining all of these patterns, you wind up with a remarkably reliable and robust state management tool. Once I'd implemented all of my logic, my store (representing the entire game's logic) came to about 250 LOC despite me not taking many efforts to minimize that metric. Although it isn't very large, it's very robust, easy to read or change, and extremely straight forward to test.
I'm sure some leetcoder could point out some really bad ideas in here and cut the logic down quite a bit. I'm alright with that. Check out the entire store implementation here.
On emitted events
One final thing I wanted to touch on is the emitted events I defined which don’t get sent to the store. As of version 2.4.0, a store can emit events—which has important implications within the XState ecosystem—but also for the standalone stores as well.
Emitted events are kind of like "fire and forget" events. If something is interested in listening to them it can do whatever it want with them, and if it isn't interested that's fine as well. They're great for allowing consumers to opt in to knowing that certain things have happened, even if the store logic doesn't care about it.
In the case of minesweeper, I'm using these emissions to let the UI layer implement handling of when the player wins or loses. For example, take a look at the tick
event handler:
I wouldn't want my tick
event to have an opinion on how the consumer (the UI in this case) handles this information, but if a consumer of the store wants to, they now have the option to hook into that event:
Again, matching on possible event values mean if this event ever changes, I’ll know immediately and be able to update the handler accordingly.
Testing revisited
I made a note about testing earlier, and I won't go into it too deeply here. Something I noticed while working on this though is that this way of modelling state, while not quite as rock-solid as a state machine, is very nice to model tests around. Once you have your context events defined, it becomes relatively clear what kinds of states you want to validate and the edge cases you want to rule out.
Really, the process of writing the integration tests for this game took very little time and once I was finished, changing implementations very rarely caused the tests to fail. It's a really nice pattern.
Give it a try!
You can try this game out yourself here (don't mind the ugly UI), and check out the repository here.
I've had a ton of fun experimenting with this store and I'm going to be using it a lot in the future. It's a great blend of features, and the resulting performance for most applications would be excellent. I really can't see a reason not to check it out.
What would I like to see in @xstate/store?
At the moment it's a great library, but there are a couple things I'd love to have:
Type-safe event transitions
If I could guarantee that an event can only return the context in a certain state (Or a partial context matching a certain state), this could lead to super-tight and reliable transitions. What I've got above is good enough and likely better than most state management I encounter in the wild, but it could be better at the type level without much application-level ceremony required. That would be awesome.
I've taken a crack at making this possible to a degree, and it's fairly easy to accomplish by removing the option to assign partial context values to the store, but that's a massive departure from the current feature-set. I considered something like a createStrictStore
function which returns a store that only allows complete assignments, but it seems awkward as well. It does allow for slightly safer transitions if used with discriminated unions of your valid states, but there's nothing stopping you from returning a "valid state" in the wrong transitions.
So, ultimately the best case scenario would be the ability to define that in x transition I can only return y context.
Some kind of middleware
I don't recall why it occurred to me on this project, but it struck me that it would be awesome to be able to pass middleware to the store.
At the moment you can subscribe to a store and watch changes externally, but I think what I'd like to see is some kind of pipeline internal to the store which allows you to (optionally) intercept and process events, emissions, and assignments. The intent wouldn't be to modify the underlying store logic. Instead I think I'd like to see it provide an API for adding guards, "always" or "after"-like actions, inspection and logging, persisting state, and so on.
I think there's a way this could be opt-in and very useful without making the store too complicated/losing its appeal. Regardless, it's good enough as it is. I really like it.
The end
How did you get this far? You must really like minesweeper.