01 June 2018

Making Tetris With Surplus

Play tetris

Making a tetris clone has become my goto project when learning new languages/frameworks. This was mostly a port to Surplus of a clone written for React + Redux.

The weirdiest thing about the switch was probably figuring out Surplus lifecycles and rewriting state updates that were no longer asynchronous. There was a good drop in the bundle size (from 100 KB to 50 KB) after the rewrite. I didn’t notice if there was a performance improvement/degradation, but Tetris doesn’t have very high requirements.

Managing State

State is managed in Surplus by S.js using signals and S computations. When you evaluate a signal on an JSX component attribute, the Surplus compiler wraps the evaluation inside an S computation. These are used by the runtime to track dependencies and update the DOM in response to changes in state.

For the most part this maps nicely to how React’s functional components update. It’s slightly trickier creating stateful components because Surplus components are just syntactic sugar for function calls. This requires that you are more aware about the signals used by the surrounding context. This puts an additional burden on the programmer, but you also need to take care of unnecessary renders in React, though the component lifecycle can at least let you separate mounts from updates unlike Surplus.

The following is a small example of how the evaulation of a signal can affect a component’s lifecycle in Surplus.

// MyComponent is recreated everytime count changes

let count = S.data(0); 
...
<MyComponent counts={count()} />


// Updates to count will not recreate MyComponent
let count = S.data(0); 
...
<MyComponent counts={count} />

I found the first pattern useful as a natural way to control the lifetime of certain components. In the game, there’s internal state used to track animations on the ControlledTetra component. This component is used to draw the current tetra piece. One of the props passed to this component is a signal that stores game state about the current tetra piece. This signal is evaluated as an attribute like the second example above to recreate a new instance (DOM and JS) whenever the controlled tetra piece is swapped or otherwise replaced. This was a nice way to ensure there was no leftover animation state from the previous tetra piece.

Unexpected Infinite Cycling

One of the more annoying things I would do is accidentally create a cycle between two S.js DataSignals causing S.js to get stuck in an endless update spiral. Combined with logging output, it’s a great way to lock up the page. It was pretty frustrating to see a blank page and then an absolute dump of info once the dev tools was opened. Pretty much had to keep the Chrome Task Manager open all the time.

Replacement Callbacks For DOM Manipulation

In order for things like FLIP to work you need to access the DOM before relayout (or exactly after). React’s componentDidMount and componentDidUpdate callbacks are useful here because they are called in a single execution block after the DOM is updated. This can allow low level DOM modification needed before the layout is updated. Surplus doesn’t have lifecycles, so these callbacks need to be added manually.

One option for a replacement componentDidMount is put a function in the DOM itself ensuring it executes when the DOM is ready as well as during the same execution block. It’s important that no dependencies on signals are created in that function so that onMount is not called again.

<div>
    <div ref={divRef}>
        <label>Bla</label>
        More bla
    </div>
    {onMount()} // Previous div will be created by the time this is called.
</div>

componentDidUpdate can be more interesting. The most common scenario would be to pass a reference to a DataSignal and then create another DataSignal that will shadow its value.

function UpdateComponent({signal}){
    let innerSig = S.data(S.sample(signal));
    
    S.on(signal,()=>{
        innerSig(signal());
    });
    
    return <div>{innerSig()}</div>
}

The example is a little contrived, but you could now add other properties to that component without its lifecycle being affected by changes to signal. What I like about Surplus is that you can just use signal directly in the JSX of UpdateComponent and the inner JSX will just update without tearing down the entire component. In fact, in many cases there’s no reason to create additional state like we would have done with React. Especially when we can add helper functional components to contain complex DOM operations related to that signal.

Reference

www.colinfahey.com Excellent Tetris reference. Absurd amounts of info about Tetris, its history, implementation, and more.