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:
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:
- There is no JSX or another specific syntax
- Forest doesn't require
return
from components - 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:
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:
<div data-first={1} data-second={2} />
// instead of, ex:
<div data={{ first: 1, second: 2 }} />
Look at the DOM API:
element.dataset.first = 1
element.dataset.second = 2
Forest:
h('div', {
data: { first: 1, second: 2 },
})
Event handlers
JSX compels libraries to invent the wheel with the naming of the props:
<div onClick={fn1} onClickCapture={fn2} />
But we have beautiful DOM API:
element.addEventListener('click', fn2, { capture: true })
Forest:
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?
<div style={{ height: 'auto', maxHeight: '100px' }} />
I need to remind, that inline styles allows to pass css custom properties:
<div style="height: auto; max-height: 100px; --color: black"></div>
Forest:
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
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(){}
:
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.
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.
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.
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?
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:
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. It is based on browser API classList
.
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:
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:
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 ⭐️
Top comments (0)