Show HN: A tiny and fast reactive observables library via functions
github.comLooks interesting...
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 :)
Preact, Mithril, Zod, etc. There are plenty of projects that have remained relatively light by saying “no” repeatedly to things which are outside their scope or would introduce unwarranted bloat.
Reminds me of https://suckless.org
Did they really ??
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.
You're doing something wrong. I just tested now. Preact 8.5.0 is 3.2kb minified+brotlied. Preact 10.8.2 (latest) is 3.7kb. That's a very small increase.
I think you right. I just downloaded the "tagged software version" from github.
Thanks and love the feedback. I know the dev space is full of cliche and overused terms. It's tough to not use them even when you're aware. I don't see it as a hard rule to avoid them and I think in some cases it's okay.
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.
I'm not sure that's really true for something like this, I've been working on something similar myself in these past few months, it got to about 5kb min+gzipped with all things included, and there's _a lot_ included, but if you only include the functions provided by this tiny library I wouldn't expect it to be much much larger than it, maybe a 2x or something? That's still small, fixes and performance optimizations tend to cost something, but you will never have to pay 10kb to use those 6 functions basically, the rest will be tree-shaken off.
I still don't mind people calling it light if it is though; maybe it gets removed later, however when it (still) is light, it makes it easy for me to search for so I can prevent overall project bloat.
This is only 850B, to me that was useful info that was mentioned in the headline. Yes it might get larger in the future or it might not.
Hey everyone! I put together a functional reactivity library that uses observable functions as it's primitive for tracking state and changes. It's super tiny (850B), fast, only re-computes the dirty parts of a computation tree, batches updates via a scheduler on to the microtask queue, and works in both browsers and Node. Love any feedback :)
Do you have an implementation of the cellx benchmark? [0] It'd be interesting to see how it will perform there.
[0]: https://codesandbox.io/s/oby-bench-cellx-s6kusj?file=/lib.js
Thanks for sharing that, I just put together one [0] but hard to extract meaningful data without running at least an average across X (~1k?) attempts. Nonetheless, it seems to perform extremely well based on surface metrics.
[0]: https://codesandbox.io/s/maverick-js-observables-bench-cellx...
why not $a(20) and $a(prev => prev + 10)?$a(); // read $a.set(20); // write (1) $a.update((prev) => prev + 10); // writeIt may be as a safety measure, it's much less likely to misuse the setter if it's divided into two different functions. If it's just one function you need to constantly ask yourself "wait, am I setting a function or not?", unless you always want to write `$a(() => value)`, which would be pretty ugly.
I started with that but then I thought about what if a callback or some other function is stored as an observable (i.e., like `useCallback`). Also, as fabiospampinato mentioned it's a cleaner seperation of get/set which makes it much easier to differentiate at a quick glace. You can't easily mess it up. I'm still debating whether to achieve what you're after. Maybe I have something mentally twisted and it's easier than I think.
I agree, this would make it a lot cleaner.
Nice! I love these tiny libraries. Some random thoughts:
- 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).
> I don't entirely understand what it means to dispose of observables, is that just an internal optimization for cleanups basically?
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
Thanks for all the feedback, love 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.
> 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.
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.
You're 100% right and I realized a little after writing it. Added `$root` to the library :)
You might also be interested in my microlibrary trkl.js, a Knockout-like observables library that's just 383 bytes minified and gzipped:
https://github.com/jbreckmckye/trkl
I've used this for production sites where bytes down the wire is a hard constraint.
Surprising no one mentioned here about https://github.com/staltz/xstream Creaed by Andre Staltz, the creator of cycleJS and other interesting reactive libraries in JS. xstream is powerful and also much smaller in size. Give that a try.
Much smaller than ~800B? It looks like ~5kb min+gzip according to bundlephobia (https://bundlephobia.com/package/xstream@11.14.0).
Maybe worth mentioning it’s JS/TS.
Does it handle subscribing to individual values in a collection, or adding/removing values?
So I just started a new side project, and I'm using JS without a framework for the first time in over 5 years.
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.
I made a library in which state management started the same way. A simple approach is great for performance-sensitive parts and it's easy to write, but there is very little helpful structure to it. Reactivity fails very easily, consider a dependency graph like this: a -> b a -> c b,c -> d In this case, updating `a` would cause d to be calculated (and reactions run) twice. Also, update batching is absolutely necessary for anything non-trivial.
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/...
I don’t understand the purpose of effect. Seems like compute can be used in all cases since you can dispose compute as well.
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.
> I don’t understand the purpose of effect. Seems like compute can be used in all cases since you can dispose compute as well.
Not OP, but:
- You could just use computeds instead of effects, but:
> Couldn’t read only be replaced with wrapping an observable in a closure?- 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.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.
Nice!
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.
Nice. Reminds me of a library I wrote many years ago using JavaScript Proxies.
Another notable reactivity lib is Vue's reactivity parts.
Vue's is especially nice in that it handles deep and imperative updates.
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().
So... S.js[0]?
Cool! This reminds me of S.js [0] which I've used a decent amount to great effect, but it seems about half the size. I'll have to look at how they compare (though if someone knows off the top of their head that'd be appreciated). S.js is nice because it has a helper library (surplus) for dom things.
Thanks for sharing! Looks great!
Thanks damsta!