avkonst/hookstate

Simpler and more intuitive way to do attach(Downgrade)

Closed this issue · 16 comments

We tried to implement hookstate into our serverless app and we needed to send objects from the state to the firebase database. However, in order to do so, we needed to do the following:

const data = myState.attach(Downgraded).get()

This has one main issue, which is the reason we scrapped hookstate in favor of something else. It's not intuitive, a person who hasn't read the docs a couple of times won't know what is going on. It's also really counter intuitive how .get() returns you a proxy, which can't be used directly in JSON API request. I get that there has to be a reason to force everything to need Downgraded attached to get the pure value, but the way it has to be done is ugly. I suggest creating a function toObject() which can attach Downgraded internally, but it just has better code flow. The example from above would become the following:

const data = myState.toObject().get()

or

const data = myState.get().toObject()

Hi, thanks for the suggestion. Such an API improvement is possible. However, let's root cause the problems first.

"It's also really counter intuitive how .get() returns you a proxy"
Proxy should behave exactly the same as the underlying state value, with an exception of equality operator. Proxy is used for tracking used states of segments to optimize re-rendering.

".get() returns you a proxy, which can't be used directly in JSON API request" Could you please elaborate a bit more? I never had problems with serializing JSON.stringify(state.get()).

"everything to need Downgraded attached to get the pure value"
If you always use Downgraded plugin before get, there is something is not right. It is more of an exception than a rule. Also, you can attach Downgraded plugin once to the root state after useState call and have get() returning the original value for the root state and all children states. So no needs to attach downgraded always before get(). Would it work for your usecase?

The thing is not that it doesnt work, its just that is sometimes hard to use, especially when refactoring from react state hooks to hookstate.

Proxy should behave exactly the same as the underlying state value, with an exception of equality operator

That is true, yes, when using it in React, no issues there, very pleased with the API. However, issues start to appear when you try using that state and sending it to other services.

For context, we tried converting some global states from react contexts to hookstate

We have many methods which accept objects (usually react states), which then are used in various ways, mainly API calls. When calling those functions with myState.get() as parameter instead of the old myState, a proxy is passed which then down the line breaks somewhere due to it being a Proxy. So the only straightforward solution was to attach Downgraded plugin before calling .get(). JSON.stringify wouldn't work in this scenario as we need the object down the line. And JSON.parse(JSON.stringify()) is ugly and not working well with TypeScript types unless you put some more effort there.

Also, you can attach Downgraded plugin once to the root state after useState call and have get() returning the original value for the root state and all children states.

Yes, however that seemed to be not correct by the documentation, as you lose various optimizations. We don't want that because we need to use the pure value N times in some methods, we can just Downgrade it whenever we need it to. And there comes the problem - its not a problem that we have to do it, it is the way it has to be done. It's not easily understandable what it does, unless you go find the documentation and read about it. On top of that, you need another import for the Downgraded plugin, you need to explain to new colleagues what is attach(Downgraded) and why it has to be done and so on. In my opinion its just bad developer experience, nothing else.

It can greatly simplify the learning curve, debugging and docs for such use cases.

I would like to second this too.

Just as @mutafow explained, the proxy is not an issue when using internally, but causes problems when the value is sent to other libraries (for eg, passing treeData prop from the state for react-sortable-tree package) or sometimes even when passing data down the DOM.

Right now, I am handling this issue by using this util function which will extract and return the data from the proxy object,

export const rawClone = <T = unknown>(data: T): T => {
    return JSON.parse(JSON.stringify(data));
};

Hopefully, something similar can also be added as an API to the state object which would greatly reduce the number of function calls we have to do when getting data from the state. I refrain away from Downgraded as I still need to be able to use the API functions that come with the state object, but in certain instances, I need just the raw data.

No problems. What would be the best name for the new property? I am thinking of giving it the name origin?

@avkonst origin is good with me.

A few alternatives would be raw, bare, or downgraded I guess.

Btw the library is freaking awesome! Always a joy to manage state with it!

Definitely! My colleagues have all moved to Hookstate and I write about the library on reddit any chance I get. It's really tragic how many people don't even know this exists, and keep implementing hacks with other libraries to do the things that are natively supported here.

Also yeah, an intuitive name is kinda hard to come up with, but origin feels a slight bit misleading at times, cause it almost means "starting point" while what we are looking for is "proxy-detached copy", I guess.
I'd still vote for raw though, even if it's generic.

Will let you know if I find something better 👍

arbob commented

No problems. What would be the best name for the new property? I am thinking of giving it the name origin?

I'd vote for a simpler and intuitive state.value.get().

'd vote for a simpler and intuitive

Unfortunately get() is already reserved. Unless we break backward compatibility this name is impossible

vbuch commented

Hi there.
Thank you for the great lib. Pleasure to use!

I saw the suggestions above and I think the most "native" naming would be myState.getObject() or as you seem to be looking towards the "origin" word, myState.getOrigin(). Still, I don't think "origin" is the correct word as it undergoes some potential transformation from within the Downgraded plugin. If it was const a = something; return a; that sounds like an origin. But it is rather const a = something; const b = plugin(a); return b; and that does not sound like an origin. So I would name it either getObject or toObject. Either way, you would expect that some action was performed, some casting was done and then you get an Object.

+1 on this feature. Being able to snapshot/serialize in a more straightforward manor would be great. Loving this lib!

Yeah that'd be awesome, I vote for raw as it does not conflict with anything. Also I believe it would be a function call like .raw()? I think it make sense, cuz using getters confuse me on whether I'm simply accessing a value in memory or it's doing "more work"

candidates for new State method or property: unwrap, value.get(), data, raw.

It will get addressed in Hookstate-4

Hookstate 4: State method get has options. One of it allows to remove proxy wrapping:

let state = useState(...)
state.get({ noproxy: true }) // returns the value state without proxy wrapping

New Extensions API allows to add extension methods, so it is easy to add an extension method which would do get({ noproxy: true }) inside. An extension method or property can have any name, so anything of the following could be possible:

let state = useState(..., () => MyExtensionToGetValueWithoutProxy())
// assuming the extension implements the following methods, any name is possible:
state.unwrap()
state.raw()
state.data
///....

Documentation updates are pending.