One critique is "light/tiny/lightweight" whenever I read about software being "light" I cringe and get an eye-twitch(ok not really). But adding "light" or claiming your product is "light" means usually nothing other than its new.
Sure it might start out as small/lightweight but usually this is because the software is new.
Once you start adding in corner-cases (the ones you haven't even thought about) some iterations of bug fixes (hey we all human programmers in the end), mix in a good dose of pull-request from helpful contributors (User:'Your library looks great, if you accept this PR it will fit my needs perfectly.') and finally bring it up to par with a competitor of two (over time of course !).
Your now light software is no longer light !
What is the answer ? To be honest I don't have one, but to say knucckle down and be sure to say no often enough and have a laser focus on the exact problem your software is solving. Of course it seldom the case in real life with real software :)
Anywhoo ! Congrats on actually releasing something :)
Looking at latest version of Preact v 10.5.14.zip (887kb) vs Preact v8.5.0.zip(90kb). So that is almost a x10 size increase in two years.
For example, if the core goal of the library is to achieve <2kB and you've predetermined a tight API scope (most likely based on existing references) then IMO it's okay to call it tiny/lightweight. It works out with libraries that are a providing a set of primitives to solve a generalized problem. I'm sure some people appreciate like I do when a library clearly indicates a core goal that was achieved (i.e. size) right in the top-level description.
[0]: https://codesandbox.io/s/oby-bench-cellx-s6kusj?file=/lib.js
[0]: https://codesandbox.io/s/maverick-js-observables-bench-cellx...
$a(); // read
$a.set(20); // write (1)
$a.update((prev) => prev + 10); // write
why not $a(20) and $a(prev => prev + 10)?- I like that the effect function returns a disposer.
- I don't entirely understand what it means to dispose of observables, is that just an internal optimization for cleanups basically? Exposing an API for this feels a bit risky, like it feels easy to misuse.
- Batching everything feels interesting, no "am I in a batch or not?" problems anymore, though I quite like when things are just executed immediately, I personally prefer to opt into batching only sparingly and for performance reasons.
- A function for creating roots seems missing, I think that's important.
- "isComputed" feels like a weird function to have, maybe it should be called isReadonly since there's a readonly function too and that's what the computed gives you basically? Also maybe there should be an "isObservable" function too?
I've made something similar myself, also inspired by Solid and Sinuous, it started as a fork of Sinuous' observable actually: (https://github.com/vobyjs/oby).
In MobX there are various things that have to be manually cleaned up at different times to avoid memory-leaks, since reactive systems like this involve a lot of cross-references. I could imagine eg. that cleaning up an observable in this library releases references to all memoized values that were derived from it
> I don't entirely understand what it means to dispose of observables, is that just an internal optimization for cleanups basically? Exposing an API for this feels a bit risky, like it feels easy to misuse.
It's a cleanup function that ensures that the observable can be garbage collected by clearing references to it in the dependency sets. It also marks the observable as disposed so that it's no longer reactive and ensures no new references can be made. I think brundolf gave a really good use-case for this API but you're right people can and will definitely misuse it... ¯\_(ツ)_/¯
> I personally prefer to opt into batching only sparingly and for performance reasons.
I'll definitely look at providing an API to provide a custom scheduler. I have to be careful not to bump up size in doing so _might_ be tricky.
> A function for creating roots seems missing, I think that's important.
I don't think it's needed here because it can be created with an `$effect`. It returns a `stop` function that can be called with `true` to dispose of all inner computations. I think I should provide a `$root` function that's just sugar for the mentioned.
> "isComputed" feels like a weird function to have
Ye I agree. I'll remove it and expose `isObservable` and `isReadonly` functions.
> I've made something similar myself, also inspired by Solid and Sinuous...
`oby` is super cool. It's so feature packed that it's going to take some time to dig through all of it. I love finding libraries like these, thanks for sharing it.
Yes, but there's another ingredient missing, which is the important one: roots are not disposed of automatically when the parent computation is re-executed/disposed. That's unimplementable on top of other functions because they just don't have that property.
https://github.com/jbreckmckye/trkl
I've used this for production sites where bytes down the wire is a hard constraint.
Does it handle subscribing to individual values in a collection, or adding/removing values?
I poured over several state management libraries before just deciding to write a naive solution on my own. I've got a very simple stateful class - the data model itself is private, and it has a custom getter/setter for access. The setter broadcasts a custom event for each top-level key that changed.
Super simple, easy to understand, and the limitation forces me to keep my state as shallow as possible.
Am I missing something? I expected to run into trouble but so far it's been easy breezy.
At least for what I made, I don't think it's too complex for what it provides. It transparently integrates with the event loop and only runs what it needs to, at the cost of (min-brotli) 432 bytes at the moment. Compare the "naive" SimpleReactive with the complete FunctionalReactive version here: https://github.com/Technical-Source/bruh/blob/main/packages/...
Couldn’t read only be replaced with wrapping an observable in a closure?
const $a = $observable(42)
const $b = () => $a()
IMO “use after dispose” should crash to make it easier to detect bugs. Also dispose should error when it texts a request to dispose something that still has a dependency. It is unlikely this is intended as it would likely lead to a “use after free” or memory leak.
Not OP, but:
- You could just use computeds instead of effects, but:
- Effects don't have an internal observable, so they are cheaper to make and to keep in memory.
- If OP will implement something like Suspense they'll probably want to pause the execution of effects, but not the execution of computeds.
- OP didn't implement this, but potentially you could support returning a cleanup functions inside effects, you can't do the same for computeds.
> Couldn’t read only be replaced with wrapping an observable in a closure?Yes, internally it's probably just a `$computed( $observable )`. The differences are that one is just a function and the other is an observable, which is a detectable difference that may matter, but also potentially the utility function could return you always the same observable if it has already a reference to a read-only version of the one you give it, which may consume less memory (1 function to keep around rather than N), though this is a bit of an edge case.
Add me to the list of people who've made their own "tiny" observable js lib (https://github.com/kevinfiol/vyce). Albeit, this one looks more complete.
For example: you have an observable foo, and it has an array bar. You can read foo.bar directly instead of needing a wrapper getter function. You can also update foo.bar = … instead of needing a setter function. You can also call foo.bar.sort() and Vue will pick this up.
I see lots of small reactive libraries but a lot of them will not pick up something like foo.bar.sort().