classList API in forest
Tags: javascript, effector, forest, typescript
Published: 2022-04-04 11:20:39

[Forest](https://www.npmjs.com/package/forest) is a reactive JavaScript/TypeScript rendering engine based on an effector — business logic manager.
The main idea of the forest is "initialize once, never render". Here we don't have such things as to render calls like in React.

Let's show you an example:

```ts
import { h, using } from 'forest'

function App() {
  h('div', {
    text: 'Hello',
    fn() {
      h('span', { text: 'World' })
    },
  })
}

using(document.querySelector('#root'), App)
```

I need to highlight 3 things:

1. There is no JSX or another specific syntax
1. Forest doesn't require `return` from components
1. Each function/component is called only once

Let's explain:

## There is no JSX or another specific syntax

JSX looks like HTML, but do more with javascript embeds. But, when we trying to pass something that not looks like attributes, we need to build tricks:

```tsx
function ReactComponent(props) {
  return <div
      onClick={handleClick}
      data-someid={props}
      style={{ height: props.height }}
      {...props.extras}
    >content</div>
}
```

Please note that we also have CSS custom attributes accessible from javascript. We can customize handlers setup (capture, preventDefault, stopPropagation, and so on). DOM API also supports setting many text nodes.

But JSX doesn't support them and for already solved tasks we have a very strange API.
Also, JSX requires passing all kinds of properties to the same place, to the same object, and it shifts responsibility to solve conflicts on end developers.
Also, any specific language requires installing specific tools and learning how to solve specific issues with the new language.

### Data-attributes

JSX has anti-DX:

```jsx
<div data-first={1} data-second={2} />
// instead of, ex:
<div data={{ first: 1, second: 2 }} />
```
Look at the DOM API:

```jsx
element.dataset.first = 1
element.dataset.second = 2
```

Forest:

```ts
h('div', {
  data: { first: 1, second: 2 },
})
```

### Event handlers

JSX compels libraries to invent the wheel with the naming of the props:

```jsx
<div onClick={fn1} onClickCapture={fn2} />
```

But we have beautiful DOM API:

```js
element.addEventListener('click', fn2, { capture: true })
```

Forest:

```ts
h('div', {
  handler: { click: fn1 },
})
h('div', {
  handler: { config: { capture: true }, on: { click: fn2 } },
})
// or in the same element
h('div', () => {
  spec({ handler: { click: fn1 } })
  spec({ handler: { config: { capture: true }, on: { click: fn2 } } })
})
```

### Style properties

Ok, inline styles are not the best practice, but we have cases for them. What JSX can offer us?

```jsx
<div style={{ height: 'auto', maxHeight: '100px' }} />
```

I need to remind, that inline styles allows to pass css custom properties:

```html
<div style="height: auto; max-height: 100px; --color: black"></div>
```

Forest:

```ts
h('div', {
  style: { height: 'auto', maxHeight: '100px' },
  styleVar: { color: 'black' },
})
```

But JSX in React with TypeScript doesn't allow passing CSS custom properties without tears.

### Whole picture

```ts
function ForestComponent({ fn, someId, height }) {
  h('div', {
    handler: { click: handleClick },
    data: { someId },
    style: { height: height },
    text: 'content',
    fn,
  })
}
```

Note, that each kind of properties and attributes is separated from each other, we don't need to solve conflicts. But also, in the forest way in the most cases we don't need props at all, just call `spec()` inside `fn(){}`:

```ts
function ForestComponent({ fn }) {
  h('div', {
    text: 'content',
    fn,
  })
}

ForestComponent({
  fn() {
    spec({
      handler: { click: handleClick },
      data: { someId },
      style: { height: height },
    })
  },
})
```

> Yep, we like OCP from SOLID, and props allow it. But as you can see, you HAVE a choice.

## Forest doesn't require `return` from components

React and JSX requires returning some elements from components because this library builds a diff-tree to compare with the previous view-state. Forest uses the same mechanism as react-hooks to solve the same problem.

It is called declarative stack-based DOM API. [Proof of concept implementation](https://gist.github.com/zerobias/f8a5701b9f33f8ce60d5757775b1453c).

When you called `using` with some `App` function, `forest` saves each call of `h`, `spec`, `list`, and other methods to the stack, it looks like virtual DOM, but you don't need to compare tree with each other. There is no render. Forest knows each point where is reactivity is applied: the user just passed `effector Store` instead of a literal value. When the store is updated, forest batches change and apply it to a DOM with 60 frames per second rate. Concurrent rendering out from the box.

This is why **each function/component is called only once**. And this is why you as a forest user need to change your habits to design forest components. You can't just put `if` where you want, you need to use reactive `Store` and `visible` properties to declaratively show/hide any element you want.

# What was before `classList`

I need to explain how the `spec()` method works.

```ts
h("input", {
  attr: { type: "number" },
  fn() {
    spec({ attr: { class: "w-full" } })
  },
})
```

All method called inside `fn(){}` property applies on the `input` element created by `h()`. So, we can create children elements if we call `h()` inside `fn(){}`.

`spec()` just add new properties, handlers, and so on on the already created element. In our case we should have `<input type="number" class="w-full" />`.

But with string attributes, we will have some problems.

```ts
h("input", {
  attr: { class: "first" },
  fn() {
    spec({ attr: { class: "second" } })
  },
})
```
What value we should set for the `class` attribute? First or Second? Or we should merge? If merge, what if I need to override or disable some classes?

```js
function Component({ fn }) {
  h("input", {
    attr: { class: "w-full text-red" },
    fn() { fn() },
  })
}

Component({ fn() {
  spec({ attr: { class: "w-20 text-blue" } }) // Oops! We already set w-full and text-red
}})
```

There we have the same problem with reactivity:

```js
function Component({ $enabled }) {
  const $class = val`w-full ${$enabled.map(is => is ? 'text-red' : 'text-gray')}`;
  h('input', {
    attr: {
      class: $class,
      type: 'number',
    },
  })
}
```

# What do we have now?

New API allows to set up each class independently. [Proposal](https://github.com/effector/effector/issues/599). It is based on browser API [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).

```ts
h("input", {
  classList: ["first"],
  fn() {
    spec({ classList: ["second"] })
  },
})
```

Now classes can be combined, because forest operates each class independently, instead of a string of something inside.
Also, you have reactivity out from the box:

```ts
function Component({ $enabled }) {
  h('input', {
    attr: {
      class: "w-full",
      type: 'number',
    },
    classList: {
      'text-red': $enabled,
      'text-gray': $enabled.map(is => !is),
    },
  })
}
```

Attribute `class` and `classList` will be merged. And nested `spec()` call also supported:

```ts
function Component({ fn }) {
  h("input", {
    classList: ["w-full", "text-red"],
    fn() { fn() },
  })
}

Component({ fn() {
  spec({
    classList: {
      "w-full": false,
      "w-20": true,
      "text-red": false,
      "text-blue": true,
    },
  })
}})
```

Thank you for the reading! 🧡 ☄️
Forest is still in the development stage, you can help us improve its API or ecosystem: [github.com/effector](https://github.com/effector/effector) ⭐️