Today’s Monday, so I thought I’d lay out exactly what I need done by the end of this week.
- Have a working animated rive board by Thursday.
- If I don’t, have any working playable board hosted on a Github page by Friday.
I expect the bulk of the work to be learning how to best handle dynamic components in React (or in spite of React), designing some code architecture to reflect that, and implementing it.
Last week I encountered the following issues:
- my
TangoTSAPI was designed around callbacks (e.g.addChangeCallbackoraddWinCallback) to allow other code to trigger animations useEffecthooks were confusing and, combined with callback patterns and using Rive components/hooks (some of which arenullinitially and rely onuseEffectto wait until they were loaded in)- for regenerated boards to flip into view (and out of view), I had to learn how react-spring’s
useTransitionworked so each board (and state) is treated as ephemeral - all the above combined together resulted in a lot of trial & error at midnight
I’d rather not repeat the above, and will instead explicitly write out all of my thoughts & reasoning. It’s a bit embarrassing to reveal how little you know about these frameworks and hooks, but I’d rather show my ignorance publicly so that I’m way more thorough in my reasoning (and maybe others can correct them).
What system of state management do I have right now?
I have:
-
TangoTSclass- contains an internal
_boardStateproperty, which holds a grid of tile states + rows + columns. - contains
addChangeCallback,removeChangeCallback,addWinCallback,removeWinCallbackwhere code can pass in a (key,callback) in order to run code when certain game events occur (such as any changes to the board, win state being achieved) - changing the
_boardStateis done immutably, with_boardStatebeing replaced every time it’s changed — this is so that when the board changes, callbacks getoldBoardStateandnewBoardStateas arguments - contains
changeTileAtIndexto change a specific tile,regenerateBoardto create a new board configuration (with the same rows and columns),resetBoardto clear player-editable tiles
- contains an internal
-
TangoRiveReact component- Accepts a
tangoTsAPIprop in order to drive its animations. - Displays a
TangoRiveBoardfor the actual game, plus auxiliary components like buttons and a timer. - Uses
react-spring’suseTransitionand auseEffecthook to hold aBoardState[]array, calledactiveBoards, that holds exactly oneBoardStateat a time (which is always whatever theTangoTSobject has as its_boardState.)- The reason why we hold an array with one item instead of just one actual
BoardStatevalue is because of howuseTransitionworks. It keeps track of ephemeral items in an array. It plays an animation (e.g. a fade in) whenever an item is added to the array, and another animation (e.g. a fade out) whenever it’s removed from the array. - Identity is determined by
key, which it “automagically” determines on its own. This is important when it comes to the “old board flips out, new board flips in” animations playing simultaneously: twoTangoRiveBoardsmay be on-screen playing their animations at the same time, and must have their own consistent identity. - Upon
TangoTScallingchangeCallbacks, if a brand new board is generated,activeBoardsis replaced with a brand new array with the new board state. If not, the currentactiveBoardsarray at index0is set to the new board state.- this makes it so that normal moves do not trigger the replacement of the entire board with fade in/fade out animations, while regenerations do
- The reason why we hold an array with one item instead of just one actual
- Accepts a
-
TangoRiveBoardReact component- This displays the board grid (the one doing the flipping and fading in/out) with each of its tiles being an animated
Rivecomponent for individual icon changes.
- This displays the board grid (the one doing the flipping and fading in/out) with each of its tiles being an animated
Currently, there are a lot of flaws: after mounting, the individual tiles in TangoRiveBoard have to wait for some rive hook to load before they’re usable, they somehow can’t hook onto the TangoTS changeCallback if the new board is regenerated since the rive hook is always null somehow even though I see the console.log()s printing them as non-null objects while simultaneously printing them as null objects right after that, probably because of how useEffect works or how function closures work…
It’s all so confusing to type out or even put into words, so this is a sign that I’ve got to learn more about these hooks & revamp my design choices.
Rive
The thing that drew me to Rive was their state machine feature. Essentially, Rive allows you to use a visual editor to create a state machine and define how animations play as state changes. This allows designers to create their own “module” of defined parameters and behaviors into a single .riv file, which is passed on to the developer to be implemented in whatever runtime environment they develop in.
In practice, because the logic of the state machine has already been defined by the designer, it should be trivial for the developer to simply hook events together. In order to use Rive components in React, most examples have DOM element events (like onClick or onMouseEnter) to trigger a code block that changes “Rive state” imperatively and mutably. To do this, you use a useStateMachineInput hook and set its value property.
Furthermore, it may not be possible to access such state upon mounting until later (from the docs):
The return value which is the state machine input may not be immediately available due to the need for the
riveinstance to resolve first. You may want to use auseEffectto watch for when theriveinstance and the return value of theuseStateMachineInputhook has value
My approach was to have every tile do a hacky setInterval loop, checking repeatedly if the object returned by useStateMachineInput. But that never really helped.
I think the biggest problem here is that the React runtime was built for very specific use cases, and mine was a step too far outside of what it was meant for. I think I’ll try looking at the plain JS web runtime.
…

…and now it works perfectly. Turns out I had a good enough idea of react hooks that I could implement the plain web runtime just fine within a react component. Now, it 1) initializes to the correct tile state, 2) properly changes on click even when it’s a newly regenerated board, 3) appropriately adds and removes callbacks from TangoTS. When I spam-regen the board, the amount of callbacks attached to TangoTS stay consistent!
However, there’s a memory leak. Apparently with each new board regeneration, the old ones stay as detached nodes. I really don’t like how I’m beholden to the black box that is useTransition and I don’t like trying to work with them, so I think I’ll attempt to make my own state without that hook.
I’d need to keep track of potentially multiple board states (spamming the regeneration button should allow you to see multiple boards flipping in and out of existence at once), their lifetimes, and handle instantiation + cleanup.
I’ll figure it out tomorrow.