Settings

Theme

Show HN: A tiny and fast reactive observables library via functions

github.com

72 points by rahim_alwer 4 years ago · 40 comments

Reader

rawoke083600 4 years ago

Looks 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 :)

  • christophilus 4 years ago

    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.

  • rahim_alwerOP 4 years ago

    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.

  • fabiospampinato 4 years ago

    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.

  • tluyben2 4 years ago

    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.

  • a1371 4 years ago

    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.

rahim_alwerOP 4 years ago

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 :)

  • fabiospampinato 4 years ago

    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

  • leeoniya 4 years ago

      $a(); // read
      $a.set(20); // write (1)
      $a.update((prev) => prev + 10); // write
    
    why not $a(20) and $a(prev => prev + 10)?
    • fabiospampinato 4 years ago

      It 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.

    • rahim_alwerOP 4 years ago

      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.

    • MH15 4 years ago

      I agree, this would make it a lot cleaner.

fabiospampinato 4 years ago

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).

  • brundolf 4 years ago

    > 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

  • rahim_alwerOP 4 years ago

    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.

    • fabiospampinato 4 years ago

      > 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.

      • rahim_alwerOP 4 years ago

        You're 100% right and I realized a little after writing it. Added `$root` to the library :)

jbreckmckye 4 years ago

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.

p2hari 4 years ago

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.

tobr 4 years ago

Maybe worth mentioning it’s JS/TS.

Does it handle subscribing to individual values in a collection, or adding/removing values?

danielvaughn 4 years ago

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.

  • wlib 4 years ago

    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/...

pyrolistical 4 years ago

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.

  • fabiospampinato 4 years ago

    > 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:

      - 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.

kevinfiol 4 years ago

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.

tristanMatthias 4 years ago

Nice. Reminds me of a library I wrote many years ago using JavaScript Proxies.

https://github.com/tristanMatthias/proxa

cjblomqvist 4 years ago

Another notable reactivity lib is Vue's reactivity parts.

  • amitp 4 years ago

    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().

junon 4 years ago

So... S.js[0]?

https://GitHub.com/adamhaile/s

afranchuk 4 years ago

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.

[0]: https://github.com/adamhaile/S

damsta 4 years ago

Thanks for sharing! Looks great!

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection