I know it's early days for this project, and perhaps too soon to critique anything - but I'm following along, as I've been very interested in Sinuous for a while now, and this promises to be a "rewrite of Sinous", and I would love to see those ideas evolve from here. 🙂
I'm looking at the example, and frankly I'm finding it a bit obscure. One of the things Sinuous has going for it, is it's reasonably intuitive to get started with - I really wouldn't like to see that get lost in a rewrite.
So here's my (full disclaimer:) really opinionated comments, take'em or leave'em. 😁
import { wS, wR, v$ } from '../src/wire/index.js';
☝ These functions need proper names.
I think I pointed you to dipole already? This has fairly intuitive naming, once you're used to the terminology: observable(value) creates an observable value, computed(() => { ... }) creates a computed reaction, reaction(() => { ... }) creates reactive effects, and so on.
I also like that Dipole's observables make reading and writing explicit with .get() and .set(value) methods - it requires slightly more work on the keyboard, but makes it much easier to skim through a large chunk of code and spot where the reads/writes are taking place. Opinionated, but readable code is more important than writable code: you only have to write it once, but you have to read it many times, your coworkers have to read it, and so on.
const data = wS({
text: '',
count: 0,
countPlusOne: wR(($): number => {
console.log('Conputing countPlusOne');
return data.count($) + 1;
}),
countPlusTwo: wR(($): number => {
console.log('Conputing countPlusTwo');
return data.countPlusOne($) + 1;
}),
});
☝ These APIs need to be consistent.
Why does wS accept an object with many observable values, and reactions mixed in?
The text and count observables, and the countPlusOne and countPlusTwo reactions, do not have any relationship with each other, as far as I can figure? I don't think there's any compelling reason to pack unrelated initializations into a single function - so I'd prefer these two functions were more consistently both singular factory functions, something like:
const data = {
text: observable(''),
count: observable(0),
countPlusOne: reaction(($): number => {
console.log('Conputing countPlusOne');
return data.count($) + 1;
}),
countPlusTwo: reaction(($): number => {
console.log('Conputing countPlusTwo');
return data.countPlusOne($) + 1;
}),
};
This is already more readable, consistent and intuitive - it should simplify typing with TS as well.
<p>Here's math:
{wR(($) => data.count($) < 5
? Math.PI * data.count($)
: `Text: "${data.text($)}" is ${data.text($).length} chars`)}
</p>
☝ Too many dollar signs 💸
I think I get what you're trying to do here, and I love the fact that the reactive context isn't somehow established by some magic behind a dark curtain.
Side story: I recently made a friend try out Sinuous, and the magic actually bit him. He had a parent component that contained a reactive expression - and in a child component that was used in that reactive expression, he thought he could safely read from an observable, in a section of the child component's code that wasn't reactive. Which you absolutely can - but the reactive expression in the parent component will pick up the usage and associate it with updates of the parent, which caused unexpected updates. It took him a very long time to fix this. That's nasty and frustrating learning curve, and it's something anybody is likely to run into at first.
Dipole has the same problem, btw.
Fixing that would be awesome.
But the syntax here is a bit ugh - manually passing that same argument again and again everywhere.
Also, you end up unnecessarily reading the same observables more than once. (which, yeah, you could assign the values to variables first, but then almost nothing dynamic would ever be just a single, elegant expression, so, ugh.)
Did you consider maybe something like this instead?
<p>Here's math:
{reaction([count, text], (count, text) => count < 5
? Math.PI * count
: `Text: "${text}" is ${text.length} chars`))}
</p>
This is even more explicit - though perhaps too explicit, and creates subscriptions on things that might not actually be used, so, meh. You might could get around that with even more explicitness:
<p>Here's math:
{reaction([count], count => count < 5
? Math.PI * count
: reaction([text], text => `Text: "${text}" is ${text.length} chars`)))}
</p>
This gives you the right subscriptions but, eh, that's not pretty.
Yeah, I'm not really sure what to suggest here. 🤔
And this last thing:
<input value={wR(data.text)}/>
☝ I know that's actually the same operation as the other reaction - that data.text is the reader function, and it'll receive the $ argument, so it's equivalent to the redundant wR($ => data.text($))... but it looks like two completely different things.
I know Sinuous does something similar, allowing observables to be used directly in expressions, where it'll create the reaction for you, but it's not a favorite feature of mine either...
I like consistent patterns in UI libraries - where you recognize operations intuitively, without parsing.
I don't know, maybe this is yet another thing you just have to get over - but it's worth thinking about learning curve and adoption rate. Not at all cost, of course - but it's not as much fun being the one guy who gets something, going through life struggling to get other people to get it, and I think that's really the key to React's success, and certainly to the introduction of hooks, even though those are absolutely horrendous and nasty behind the curtains.
I wouldn't want to trade off good or clean or correct for something magical that "looks cool", but is there something we can do to make this more intuitive and palatable?
I hope you don't find any of this too offensive, but feel free to tell me to f_ck off and mind my own business. 😄
I know it's early days for this project, and perhaps too soon to critique anything - but I'm following along, as I've been very interested in Sinuous for a while now, and this promises to be a "rewrite of Sinous", and I would love to see those ideas evolve from here. 🙂
I'm looking at the example, and frankly I'm finding it a bit obscure. One of the things Sinuous has going for it, is it's reasonably intuitive to get started with - I really wouldn't like to see that get lost in a rewrite.
So here's my (full disclaimer:) really opinionated comments, take'em or leave'em. 😁
☝ These functions need proper names.
I think I pointed you to dipole already? This has fairly intuitive naming, once you're used to the terminology:
observable(value)creates an observable value,computed(() => { ... })creates a computed reaction,reaction(() => { ... })creates reactive effects, and so on.I also like that Dipole's observables make reading and writing explicit with
.get()and.set(value)methods - it requires slightly more work on the keyboard, but makes it much easier to skim through a large chunk of code and spot where the reads/writes are taking place. Opinionated, but readable code is more important than writable code: you only have to write it once, but you have to read it many times, your coworkers have to read it, and so on.☝ These APIs need to be consistent.
Why does
wSaccept an object with many observable values, and reactions mixed in?The
textandcountobservables, and thecountPlusOneandcountPlusTworeactions, do not have any relationship with each other, as far as I can figure? I don't think there's any compelling reason to pack unrelated initializations into a single function - so I'd prefer these two functions were more consistently both singular factory functions, something like:This is already more readable, consistent and intuitive - it should simplify typing with TS as well.
☝ Too many dollar signs 💸
I think I get what you're trying to do here, and I love the fact that the reactive context isn't somehow established by some magic behind a dark curtain.
Side story: I recently made a friend try out Sinuous, and the magic actually bit him. He had a parent component that contained a reactive expression - and in a child component that was used in that reactive expression, he thought he could safely read from an observable, in a section of the child component's code that wasn't reactive. Which you absolutely can - but the reactive expression in the parent component will pick up the usage and associate it with updates of the parent, which caused unexpected updates. It took him a very long time to fix this. That's nasty and frustrating learning curve, and it's something anybody is likely to run into at first.
Dipole has the same problem, btw.
Fixing that would be awesome.
But the syntax here is a bit ugh - manually passing that same argument again and again everywhere.
Also, you end up unnecessarily reading the same observables more than once. (which, yeah, you could assign the values to variables first, but then almost nothing dynamic would ever be just a single, elegant expression, so, ugh.)
Did you consider maybe something like this instead?
This is even more explicit - though perhaps too explicit, and creates subscriptions on things that might not actually be used, so, meh. You might could get around that with even more explicitness:
This gives you the right subscriptions but, eh, that's not pretty.
Yeah, I'm not really sure what to suggest here. 🤔
And this last thing:
☝ I know that's actually the same operation as the other reaction - that
data.textis the reader function, and it'll receive the$argument, so it's equivalent to the redundantwR($ => data.text($))... but it looks like two completely different things.I know Sinuous does something similar, allowing observables to be used directly in expressions, where it'll create the reaction for you, but it's not a favorite feature of mine either...
I like consistent patterns in UI libraries - where you recognize operations intuitively, without parsing.
I don't know, maybe this is yet another thing you just have to get over - but it's worth thinking about learning curve and adoption rate. Not at all cost, of course - but it's not as much fun being the one guy who gets something, going through life struggling to get other people to get it, and I think that's really the key to React's success, and certainly to the introduction of hooks, even though those are absolutely horrendous and nasty behind the curtains.
I wouldn't want to trade off good or clean or correct for something magical that "looks cool", but is there something we can do to make this more intuitive and palatable?
I hope you don't find any of this too offensive, but feel free to tell me to f_ck off and mind my own business. 😄