It looks a lot like React hooks, and that's intentional as it's a partial inspiration, but I've extended that somewhat and gave it additional semantics with the goal to make it much more broadly useful, fulfilling many of the same functions as Backbone and Redux by making it a generic serializable state store rather than hard-coded to any particular pattern. (You can see this in action with my counters.)
This seems like a very interesting idea, and it's well thought out - I'm intrigued by its potential.
You've discussed a bit about how this concept might be used on a server, and the example you gave was basically moving UI state management logic to a server (but the results are still serialized and sent to a UI). Are there use cases for this idea that doesn't involve the UI at all?
Copied from the proposal (emphasis added):
- It glides right in perfectly with tc39/proposal-eventual-send, providing a very nice and easy backend while letting those act as a front end. It might be worth adding a built-in wrapper for these as a follow-on if that takes off, or maybe even having async actor instances implement
[[EventualSend]]
and so on. One could also imagine using it as the basis of something similar to Cloudflare's durable objects.
This is in reference to Cloudflare Durable Objects ยท Cloudflare Durable Objects docs, if it helps. Conceptually, it has a lot in common with Erlang's processes, just it's a more static form of it where Erlang's process dictionary is a very dynamic and free-form thing. You could also compare it to Akka's actors, where it shares a lot more in common. I focused on the UI side as it practically doesn't exist there, while in highly distributed and networked applications it's very popular.
The proposal says
lessons from what worked well (React Hooks' state mechanism, virtual DOM,
But did those things go well? Solid.js is faster, easier to understand, without gotchas like stale closures (function components execute only once, not over and over), without virtual DOM.
A few reasons why Solid is great in a nutshell:
import {createSignal, createEffect, render} from 'solid-js'
function MyApp() {
// Reactive variable with initial value 0
const [count, setCount] = createSignal(0)
// Increment count every second
setInterval(() => setCount(count() + 1), 1000)
// Use count in the *DOM*
const div = <div>count is {count()}</div>
// This! This is true!!!!
console.log(div instanceof HTMLDivElement) // true
// Pass it to jQuery? Sure!
jQuery(div).foo()
// Log count every time it changes.
createEffect(() => {
console.log('count is', count())
})
console.log('This log only happens once!')
return div
}
render(<MyApp />, document.body)
I need to study Actors though. I'm not learned in those yet. Brb.
Here's what that could look like with a language feature, minus the functional component and minus the JSX (left to the imagination):
// Reactive variable with initial value 0
reactive count = 0
// Increment count every second
setInterval(() => count++, 1000)
// Use count in the *DOM*
const div = document.createElement('div')
autorun {
div.textContent = 'count is ' + count
}
// This! This is true!!!!
console.log(div instanceof HTMLDivElement) // true
// Pass it to jQuery? Sure!
jQuery(div).foo()
// Log count every time it changes.
autorun {
console.log('count is', count)
}
console.log('This log only happens once!')
document.body.append(div)
Name bike shedding: autorun
could be called effect
, reactive
could be signal
, ...
Here's a computed value, this time using the name effect
in place of autorun
:
computed doubleCount = count * 2
// Log doubleCount any time it changes (use it anywhere you'd use a primitive reactive variable):
effect {
console.log(doubleCount)
}
reactive
vars are writable as with let
, computed
vars are readonly like with const
(although their values change if their dependencies are updated, just that the dev can't explicitly write to them).
The language would handle nested effects (and this is the basis for all DOM component frameworks/libs):
effect {
// This effect reruns only when hasFoo changes.
if (hasFoo) effect {
// log foo when it changes, but the outer
// effect will not rerun when foo changes
console.log(foo)
} else effect {
// log bar when it changes, but the outer
// effect will not rerun when bar changes
console.log(bar)
}
}
Where hasFoo
, foo
, and bar
are reactive
or computed
variables.
The outer effect reruns only if hasFoo
changes. Depending on which effect is reached during a rerun, no longer reached effects are stopped, and newly reached effects are started. When an inner effect is reached and started, the inner effect tracks it's dependencies and it can further reach inner-inner effects, and so on.
This is how all functional components essentially run (well, except for React, which tries to emulate this, but doesn't actually do this like Vue, Svelte, and Solid do). React's version should be disqualified from this because it causes confusion (hooks rules, stale closures from needlessly rerunning, etc).
Using these three primitives, we now have the ability to make functional components without a library:
Here's the original Solid example, but still without JSX (with autorun
instead of effect
, ):
function ComponentA() {
// Reactive variable with initial value 0
reactive count = 0
// Increment count every second
setInterval(() => count++, 1000)
// Use count in the *DOM*
const div = document.createElement('div')
autorun {
div.textContent = 'count is ' + count
}
// This! This is true!!!!
console.log(div instanceof HTMLDivElement) // true
// Pass it to jQuery? Sure!
jQuery(div).foo()
// Log count every time it changes.
autorun {
console.log('count is', count)
}
console.log('This log only happens once!')
}
document.body.append(ComponentA())
Now here's how nested effects allow component composition:
function ComponentA {
// Same as before
}
function ComponentB(props) {
const wrapper = document.createElement('div')
reactive blue = 0
// Sprinkle of jQuery (if you're into that):
$(wrapper).on('click', () => {
blue = Math.round(255 * Math.random())
})
effect {
wrapper.style.border = `${props.borderSize%5+1}px solid rgba(23, 45, ${blue})`
}
wrapper.append(ComponentA())
return wrapper
}
function ComponentC(_props) {
const props = {
reactive borderSize: _props.initialBorderSize
}
setInterval(() => props.borderSize++, 2000)
return ComponentB(props)
}
document.append(ComponentC({initialBorderSize: 3}))
// or with simple JSX sugar:
document.append(<ComponentC initialBorderSize={3} />)
As we can see, with these primitives we can do a lot, even have a UI component system (all that's needed is very simple JSX syntax to make it more terse, but without JSX no library or framework is even needed!).
We can extend arg structuring and parameter destructuring syntax to allow passing reactivity. Updated ComponentB and C:
function ComponentB({computed borderSize}) {
const wrapper = document.createElement('div')
reactive blue = 0
// Sprinkle of jQuery (if you're into that):
$(wrapper).on('click', () => {
blue = Math.round(255 * Math.random())
})
effect {
wrapper.style.border = `${borderSize%5+1}px solid rgba(23, 45, ${blue})`
}
wrapper.append(ComponentA())
return wrapper
}
function ComponentC({initialBorderSize/*(not reactive)*/}) {
reactive borderSize = initialBorderSize
setInterval(() => borderSize++, 2000)
return ComponentB({send borderSize})
}
Finally the nested effects lend to component composition:
function ComponentD() {
reactive rand = Math.random()
setInterval(() => rand = Math.random(), 2000)
return rand >= 0.5
? ComponentB({borderSize: 3})
: ComponentC({initialBorderSize: 4})
}
We can start to imagine how no-longer-reached and reached effects are started and stopped based on the ternary, and we can also imagine how it would be written with JSX sugar...
Furthermore, instead if starting and stopping effects in separate logic branches, one could spend one DOM or the other depending on which one should be visible (or toggle a class, etc). Components can handle this for you, and JSX would make it even more concise.
Everything above is essentially what Solid.js is, but with APIs and today's more limited JS syntax.
FWIW, I'm abandoning the proposal anyways - I'm no longer convinced it's a good idea.
Edit: Read up on Actor model - Wikipedia and cell-based reactive programming - those were my two largest inspirations. The syntax was inspired by React Hooks, but only inspired.
Also, the reason why I've revoked my proposal is that I've intuitively misinterpreted the core of the actor model. I've been coding a bit in Erlang lately, and of course I'm currently investigating how that changes things concurrency-wise because it handles concurrency drastically differently. And let's just say the way it inverts message processing is actually extremely useful.
This is even more React-like than my proposal. You basically just replicated React Hooks. Mine at least allowed custom handlers.
It's not like React hooks though, it's Solid/Knockout/Mobx reactive computations. There's a fundamental difference. React hooks imitates those, the runtime behavior is entirely different in React than the stuff in the above examples. React came after these concepts and invented something else to try to fit in, but it's a pretty bad fail at that (IMO).
There is a huge difference between how React works, and how the above works: in the above before the ComponentD example, ComponentA/B/C functions only ever execute once. That's a huge difference, it is totally not like React, and this form of reactivity existed before React created that monster (Knockout.js, Meteor, Qt QML, etc), and Solid.js proves this concept can be one of the fastest while also providing an amazing developer experience. See Solid in these benchmarks:
https://krausest.github.io/js-framework-benchmark/current.html
The above dependency-tracking reactivity idea is perfected by Solid.js, but Solid didn't invent it, Solid created a simple ergonomic API with JSX DOM sugar for propagating reactive variables to DOM objects.
TLDR, a proposal for what Solid.js effectively does (and what Knockout, Meteor, Mobx, Qt QML, etc do, check those out) would be nice.
Yeah, I misread your proposal - I didn't look closely enough. (And I'm familiar with MobX - one of my workplace's open source projects uses it extensively.)
I do have a question: how would it conceptually work within the framework of a virtual DOM system (like Vue) or similar (like Svelte)?
Vdom wouldn't work within this concept, rather a vdom framework could just use or wrap these primitives, f.e. it would create effects and the effects would trigger vdom diffing, etc.
In the above example where I update textContent
inside the effect, some library could similarly compile their JSX to modify a vdom tree instead of an actual-DOM tree based on changes of reactive variables, and the vdom framework could queue an update (queue a diff) from inside the effect.
It is no different than if the vdom library relied on an event emitter pattern, or something like setState like in React, or some value streaming API, to then queue updates. The depdency-tracking auto-running reactivity pattern is merely another way to observe changes, but once the changes are observed, any library can do whatever they want with the changes including update a vdom tree.
Vue works that way (reactive effects that trigger updates to a vdom tree).
Solid on the other hand uses reactive effects to update DOM directly instead of a vdom, but the auto-running dependency-tracking effects are in common with Vue, Svelte, and others.
You indirectly answered my question, but just to be clear, I was saying mostly the other way around, within the structure of a virtual DOM framework, not within the structure of your proposed construct.
Yes, VDOM framework can use this dep-tracking reactivity paradigm in its implementation.
It would be very similar to, for example, using MobX
inside of React
, but I wouldn't consider it ideal because we're mixing more concepts that we need. MobX, when used inside React, is essentially the example of what you're saying: a vdom system that can re-render itself based on reactive signals.
What dep-tracking reactivity is good for is fine-grained updates, and mapping value changes directly to places that need the updated values whenever the values change.