Open Source Adventures: Episode 44: Spooky Eyes in SolidJS
Tags: javascript, solidjs
Published: 2022-04-27 21:41:41

Let's code a very simple app in SolidJS, Spooky Eyes! [Here's what Svelte version looks like](https://taw.github.io/svelte-eyes/).

The app is simple - there's a bunch of randomly placed eyes, which follow the mouse.

I covered basic structure of the project in previous episode, here let's just focus on the interesting bits.

### `index.css`

SolidJS has support for component scoped CSS, but for simplicity I put everything in a static component.

```css
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  text-align: center;
  justify-content: center;
  align-items: center;
}
svg {
  width: 100vw;
  height: 100vh;
  display: block;
  background-color: #aaa;
}
.eye1 {
  fill: white;
  stroke: black;
  stroke-width: 3px;
}
.eye2 {
  stroke: black;
  stroke-width: 1px;
}
.eye3 {
  fill: black;
}
```

### `App.solid`

The `App` component, when created, initializes a list of randomly placed `Eye`s. It also tracks where the mouse is, and passes it to `Eye` child instances.

```javascript
import Eye from "./Eye"
import { createSignal } from "solid-js"

function randomColor() {
  let h = Math.random() * 360
  let s = Math.round(50 + Math.random() * 50)
  let l = Math.round(30 + Math.random() * 40)
  return `hsl(${h}, ${s}%, ${l}%)`
}

function eyeDistance(eye1, eye2) {
  let dx = eye1.x - eye2.x;
  let dy = eye1.y - eye2.y;
  return Math.sqrt((dx * dx) + (dy * dy))
}

function canPlaceEye(eyes, newEye) {
  return eyes.every(eye =>
    eyeDistance(eye, newEye) >= eye.sz + newEye.sz + 5
  )
}

function generateEyes() {
  let eyes = []
  let wh = window.innerHeight
  let ww = window.innerWidth
  let mx = Math.random() * ww
  let my = Math.random() * wh
  for(let i=0; i<1000; i++) {
    let sz = 20 + Math.random() * 60
    let x = sz + Math.random() * (ww - 2 * sz)
    let y = sz + Math.random() * (wh - 2 * sz)
    let color = randomColor()
    let newEye = {x, y, sz, color}
    if (canPlaceEye(eyes, newEye)) {
      eyes.push(newEye)
    }
  }
  return eyes
}

function App() {
  let eyes = generateEyes()
  let wh = window.innerHeight
  let ww = window.innerWidth
  // init it to something random if mouse starts outside window
  let [mx, setMx] = createSignal(Math.random() * ww)
  let [my, setMy] = createSignal(Math.random() * wh)

  function onMouseMove() {
    let svg = document.getElementById("eyes")
    let rect = svg.getBoundingClientRect()
    setMx(event.pageX - rect.x)
    setMy(event.pageY - rect.y)
  }

  return (
    <svg id="eyes" onMouseMove={onMouseMove}>
      <For each={eyes}>
        {({x, y, sz, color}) => <Eye x={x} y={y} sz={sz} color={color} mx={mx} my={my} />}
      </For>
    </svg>
  )
}

export default App
```

Some notable things:

* it's JSX, but unlike React we don't loop with `.map`, we need to use `<For>...</For>`. Most frameworks except React end up having special syntax for looping, if/else etc.
* we have two parts of state - `mx` and `my` mouse position. `eyes` are static so they don't need to be `createSignal`ed

Other than that, it is quite close to what we'd do in React, so learning curve for React developers should be pretty low.

### `Eye.solid`

Here's `Eye` component:

```javascript
import { createSignal, createEffect } from "solid-js"

function Eye({x,y,sz,color,mx,my}) {
  let [rx, setRx] = createSignal(x)
  let [ry, setRy] = createSignal(y)

  createEffect(() => {
    let max_eye_movement = 0.3 * sz
    let dx = mx() !== null ? (mx() - x) : 0
    let dy = my() !== null ? (my() - y) : 0
    let dl = Math.max(0.000001, Math.sqrt(dx*dx + dy*dy))
    let displacement = Math.min(max_eye_movement, dl)
    setRx(x + dx/dl * displacement)
    setRy(y + dy/dl * displacement)
  })

  return (
    <g>
      <circle class="eye1" cx={x} cy={y} r={sz} />
      <circle class="eye2" cx={rx()} cy={ry()} r={sz * 0.5} style={`fill: ${color}`}/>
      <circle class="eye3" cx={rx()} cy={ry()} r={sz * 0.2}/>
    </g>
  )
}

export default Eye
```

This is quite awkward. In React we wouldn't need any state, and any effects. We'd just take `mx` and `my` as props, calculate everything, and return the result.

Solid requires being a lot more explicit. Arguably you gain some performance, at cost of a lot of extra work.

Arguably it looks more or less like React code - Recat developer would understand what's going on - it would just be somewhat unclear why these choices were made.

### Story so far

I deployed this on GitHub Pages, [you can see it here](https://taw.github.io/open-source-adventures/episode-44/). It works very smoothly, and quite a few frameworks I tested already struggle with this app.

So far my impressions of SolidJS aren't amazing. Its reactivity requires a lot more work from developer than React or Svelte, and I don't think it's worth it for me.

I can definitely see scenarios where tradeoffs SolidJS offers make sense. It looks like React, so for masses of React developers, it should be much easier to switch to than Svelte. Without virtual DOM overhead, it should perform roughly comparable to Svelte, and a lot better than React (in principle; I didn't benchbark anything). And it's all runtime so unlike Svelte, it doesn't require any tooling beyond just JSX that's supported everywhere.

But overall I think I'll stick to Svelte for my projects.

### Coming next

In the next few episodes, we'll do some video game data analysis.