Implementing hook-like states and effects to DOM-based library
Tags: react, javascript
Published: 2019-10-14 14:49:23

Some five months ago I continued working on a years old idea of a lean DOM library that would only return DOM nodes and keep the state updated a bit like a Virtual DOM library, but without the Virtual DOM. Those five months ago I got the project into as far as to making conditional rendering possible, but then things got halted (as they do) and I returned back to the project only now.

The only thing that has notably changed within the last five months is my knowledge of React hooks: how they work, and what issues they have. While the design of hooks is quite clever you can see that they do workaround problems caused by the Virtual DOM. Notably, a lot of effort has to go into ensuring you keep references the same. In React class syntax references aren't an issue, but then you have to work with `this` a lot.

With this DOM-based library idea a lot of React ideas go to the trash bin. If we consider this:

```jsx
import React from 'react'
import ReactDOM from 'react-dom'

function Component() {
    const [count, setCount] = useState(0)

    return (
        <div>
            {count}
            <button onClick={() => setCount(count + 1)}>+</button>
            <button onClick={() => setCount(count - 1)}>-</button>
        </div>
    )
}

ReactDOM.render(<Component />, document.body)
```

You have to remember these React facts:

1. `Component` will be executed each time component is rendered
2. `useState` keeps track of state
3. `onClick` handlers change on each render

This logic simply doesn't work if you're returning native DOM elements, because you don't want to create new DOM elements upon each render. This also means `useState` equivalent has to be very different from React and adds a challenge to solve. After some hard thinking I ended up with this syntax:

```jsx
/** @jsx dom */
import { dom, State } from './library'

const count = new State(0)

document.body.appendChild(
    <p>
        {count}
        <button onclick={count.set(count => count + 1)}>+</button>
        <button onclick={count.set(count => count - 1)}>-</button>
    </p>
)
```

Oh, I guess you notice one thing: there is no component! This is one thing that I want to embrace: since we're working with native DOM nodes there is no reason to force wrap anything into anywhere. References to functions stay always the same. The above also implies `State` can be independent of a containing component, which makes it possible to share globalish state in a very different manner compared to React!

In React if you want to have state that is usable in many remote places around your app you're pretty much forced to use Context API in one form or another. You have to put a Provider somewhere above in the render tree which then provides state to other parts of the app. The only other way to get state to inner components is by passing props through the tree. Or you make a custom solution.

Going back to the work-in-progress idea, another interesting thing happens with effects:

```jsx
import { Effect, State } from './library'

const count = new State(0)

new Effect(
    ([count]) => {
        console.log('New value is ' + count)
        return () => console.log('Old value was ' + count)
    },
    [count]
)

count.set(1)
```

You can also do side-effects without component wrapping!

`count` as returned by `new State` here is not what React's hooks return with `useState`. Instead, as the syntax suggests, you get a special state class instead. It provides three methods: `current` (which holds the value), `get` (which returns the value, and can wrap a callback function) and `set` (which allows changing the value).

## Putting `ref`s together

One thing that I noticed when making `State` is that there really is no reason to have a `Ref` equivalent. So no need to `createRef` or `useRef`, simply pass a state class:

```jsx
/** @jsx dom */
import { dom, Effect, State } from './library'

const input = new State()
const text = new State('')

new Effect(
    function([input, text]) {
        console.log('Text is now', text)
        if (text === 'blur') input.blur()
        return () => console.info('Old value was', text)
    },
    [input, text]
)

document.body.appendChild(
    <p>
        <input
            ref={input}
            oninput={text.set((text, event) => event.target.value)}
            placeholder="Write something"
            size="30"
            value={text}
            type="text"
        />
    </p>
)
```

Very short'n'sweet in many ways. Also, if you write `blur` into the input field you lose focus. Always pay attention!

---

Finally, I've had only about three hours of sleep last night as I'm being sick. So this text might be a bit confusing, but if the relevant stuff above intrigues you feel free to ask more. The code that puts all the above together is slightly sloppy and has tons of edge cases that haven't been taken care of... and detection of mounting status of a component is full of holes. But here is the CodeSandbox that I'm working on!

{% codesandbox 1b0tm %}

Not the prettiest thing around, many of the things have been put or added only to test various kinds of possible ways to break the DOM diffing!