Proposal: Add get() method to Atoms
This one may be a little controversial, and I respect that you might have thought of this and ultimately preferred only having the .value getter. I personally prefer .get() as it nicely mirrors .set(...) and I honestly have found the Xoid API to be a little surprising without it.
However, this goes a little beyond personal preference because without a stable .get() method, we have been required to pass a new function to useSyncExternalStore every time, which means we must pay the small penalty of scheduling a layout effect to re-check the snapshot on every render. It's easy to see this in the shim here (as well as for useSyncExternalStoreWithSelector here), and the same is true for the real React hook (see here).
So, ultimately I think it's worth having .get() for the slight performance benefit (it adds up for hundreds of components relying on atoms) and to satisfy some folks' personal preferences (I'm sure I won't be the only one). 😀
Hi @appden , as always, a very good point. Thank you! 🙂 I feel like you're right, but I want to think this through, and I'll share my thoughts in the following days when I have time.
Meanwhile, I want to point out that, instead of atom.get, atom[INTERNAL].get could do the same kind of performance optimization for useSyncExternalStore. I didn't notice that before, I should have written it that way.
Other than the useSyncExternalStore performance, this is still a good point to consider, because a lot of developers might share the same preference as yours:
I personally prefer .get() as it nicely mirrors .set(...)
I'm also kind of think the same. I remember trying to decide between .value or .get() back then.
My concern was to have just one way of getting the state instead of multiple ways. If we neglect that (I think it's not a huge problem), I think I do have a very important concern (on naming it as get):
get(atom) (as the derived state getter) can be confused with atom.get() inside derived state callbacks. This was the main reason I decided to settle with no .get(). Recoil/Jotai was using the get word for their state getter/subscriber as well, and I'm reluctant to rename it. For me, "value" reads without subscribing, and "get" both reads and subscribes.
Preact Signals, (which wasn't around, I think, back when I decided) has a different wording for these:
For signals, signal.value magically subscribes, but peek(signal) just gives the immediate value.
I don't have a strong objection on adding a second way of getting the state. I totally get it, and I know get/set sounds very non-awkward than other possibilities, but I think atom.get() / get(atom) confusion is a strong one. So I'm positive on adding a function getter, but I'm negative on using the word "get".
What would you think of "atom.read()" maybe? I'm aware that read/set/update doesn't sound/feel good as get/set/update, but "get" will have cognitive trade-offs in a lot of people like me. Do you have other naming suggestions?
Thanks for taking the time consider this! Yes, we can at minimum use atom[INTERNAL].get for useSyncExternalStore but I think beyond nomenclature preferences, there may be similar other use cases that pop up for users where they may prefer to have a function.
I have nothing against leaving .value getter/setters. I know Preact Signals uses them and therefore there maybe compat and familiarity reasons to have those as well. Just like there are two ways to set a value (.set and .value setter), I think it'll be ok to have the same analog for getting the value.
I totally get the concern about atom.get() vs get(atom). I'm a little less enthusiastic about atom.read() considering we have atom.set(), and I wonder if instead you should canonicalize create(read => read(atom)) instead as I think it makes more sense that way actually. However (I am sorry to bring this up), I will admit that I'd personally prefer implicit dependencies that Solid, Preact, Legend, etc. so we could just do create(() => atom.get()) and it just works, then we could have atom.peek() for avoiding the implicit dependency (like Preact and Legend). Yes, it's a little more magical but IMHO much better DX because devs are way less likely to make a mistake.
Just for fun and personal learning, I took a quick crack at what implicit dependency tracking might look like in xoid (for create and effect): https://github.com/appden/xoid/compare/get-method...implicit-dep-tracking
I assume this is probably not the direction you want to go, but I gotta say I do prefer it and think it's a developer experience win. I will be curious to hear what you think. 😀
Hi @appden you had great points in this PR. Back then, I was hesitant to have get and .get, and I wasn't planning to make use of implicit tracking in xoid, in any way. Fast forward to now, both of my views have changed. I want to say that both implicit dependency tracking and the .get method will be a thing in the next versions. You're obviously a more experienced developer and it took a little bit of time for me to truly grasp the advantages of the stuff such as implicit tracking.
After this comment of yours, I have applied this to the docs actually.
I wonder if instead you should canonicalize create(read => read(atom)) instead as I think it makes more sense that way actually.
Also, a little bit after that, I have implemented a side-package. I was a bit busy with a other things, so I failed to mention this to you: https://github.com/xoidlabs/xoid/tree/main/packages/reactive
It implements computed, watch, reactive around xoid. Now, both after examining VueJS in depth, and experimenting with alternative APIs, I have noticed that I can add computed, watch, reactive as totally opt-in separate exports (to the main package xoid) for people who want to have a signal-like experience. Right now, I have a WIP branch (https://github.com/xoidlabs/xoid/tree/shave-off-2) that implements those. Summary of the most important changes:
-
createwill be renamed toatom.createwill remain in the next few versions, but we'll get rid of it until v1.0.0 with a JSDoc deprecation notice. - Atoms are going to have a
.getmethod (that does not track dependencies, only.valuewill do that) - For implicit-tracking style, there will be
computed,watch,reactive,toReactive,toAtom(This last one brings the need to renamecreatetoatom). - The bytesize cost of
import { atom } from 'xoid'will be even smaller than the current version, probably under 0.9kB gzipped, and importing them all will be around 1.5kB. -
injectandeffectfunctions are going to be exported from the package root, notxoid/setup, and they're now not limited to usage inside UI frameworks.
Sorry for answering this too late, and not sharing the advancements that occurred in the meantime. This is the shape of xoid's current vision. It's not limiting itself to be a Redux-like, non-magical library. It's going to be a@vue/reactivity-like package, but with more emphasis on minimalism and framework interoperability, way smaller, with better tree-shaking. I'm curious to hear what you think.
Hi @onurkerimov, I really appreciate the kind words, thanks!
It implements
computed,watch,reactivearound xoid.
This looks like great idea!
createwill be renamed toatom.createwill remain in the next few versions, but we'll get rid of it until v1.0.0 with a JSDoc deprecation notice.
Good call renaming to atom!
- Atoms are going to have a
.getmethod (that does not track dependencies, only.valuewill do that)
I respectfully don't agree that .value and .get() should have different behavior. I actually think .value should just be dropped (unless there's a compat story with Preact signals) to keep things simpler.
- For implicit-tracking style, there will be
computed,watch,reactive,toReactive,toAtom(This last one brings the need to renamecreatetoatom).
❤️