ctrlplusb / easy-peasy

Vegetarian friendly state for React
https://easy-peasy.dev
MIT License
5.03k stars 190 forks source link

Misleading documentation on destructuring compted properties #777

Closed yard closed 2 years ago

yard commented 2 years ago

Hey guys,

First of all, thank you very much for the great library!

I must caveat this is 100% nit, but maybe you will find it useful to correct – happy to provide a docs PR if you suggest so.

I have been playing around with various aspects of it and noticed that the docs at https://easy-peasy.dev/docs/api/computed.html#computed-properties-break-when-destructuring-a-computed-property-out-of-state, although being correct in general, provide an example that actually is not flawed.

The thing is that when a computed property getter is pure and depends solely on the sub-state it's defined in (just like in the example where isLoggedIn only changes when user of session sub-model changes), the deconstruction will work just fine. The comparison function in useStoreState will receive the whole sub-model state, will notice it changed and will trigger a re-render, leading to computed property working correctly.

What will break though is if a computed property is impure w.r.t to the sub-model it's defined in (e.g. depends on another sub-model). It's not straightforward to achieve (the default use of computed helper supplies local state only), but of course possible when state resolver is involved:

const storeModel = {
  user: {
    current: null,
  },
  session: {
    isLoggedIn: computed((_, storeState) => [storeState.user], (userState) => userState.current != null),
  },
};

The reason I am writing about this at all is because it's very convenient to use the deconstruction in certain cases and it's a pity the documentation effectively bans that for computed property – but I appreciate it might be perceived easier than mentioning specific conditions under which it is ok or not ok.

jmyrland commented 2 years ago

Hi @yard ! 👋

We still got some work to do in regards to the documentation. Any help to make things clearer is greatly appreciated 👍

I have been playing around with various aspects of it and noticed that the docs at https://easy-peasy.dev/docs/api/computed.html#computed-properties-break-when-destructuring-a-computed-property-out-of-state, although being correct in general, provide an example that actually is not flawed.

I don't think that section is relevant anymore. I'm using computed props in production code atm, and I am destructing these props as if they were regular state.

What will break though is if a computed property is impure w.r.t to the sub-model it's defined in (e.g. depends on another sub-model)

I don't even think this is the case. We've recently done some work to optimize computed props - so I think the "Computed properties break when destructuring a computed property out of state"-section can be removed (atleast for v5).

Or do you have any issues with computed props this perticular case?

Thanks for taking the time to write this up 👍

yard commented 2 years ago

Hola @jmyrland!

Judging by my tests it seems to still be relevant – I assembled a minimal example at https://codesandbox.io/s/easy-peasy-example-forked-9nj50p. Uncomment line #7 and comment line #6 in app.js to expose the issue.

This is expected, though: returning a sub-model from state selector function that does not change upon store update leaves no way to detect that a computed property has actually changed (unless each and every property gets traversed and explicitly invoked, but that's a pretty terrible idea performance-wise).

I wonder if there's a smart way to detect that (couple thoughts come to mind), but I am not sure if it is a good idea to chase that at all...

jmyrland commented 2 years ago

Nice, thanks for taking the time to create a sandbox 🙏 I get an error for that sandbox, because of the computed definition (unrelated to this issue), but I fixed it in this one.

But, you are totally right - destructing the computed prop does not work in this case. Haven't seen this issue before 🤔

Do you see a reasonable workaround for this @ctrlplusb ?

yard commented 2 years ago

Ouch, sorry my bad with the typo - somehow it wasn’t the latest version :-/

A hacky hack that comes to mind is as follows: upon store change re-evaluate state resolver functions and place their return values into a corresponding sub-model (prefixed by _ or smth). This is ofc dirty and will imply a performance penalty.

Actually, modifying the sub-model state is probably the only way - this is what gets compared as a result of mapState callback, and it needs to be different for re-render to happen. Another way would be changing the semantics somehow, but that’s even worse I think.

jmyrland commented 2 years ago

A hacky hack that comes to mind is as follows: upon store change re-evaluate state resolver functions and place their return values into a corresponding sub-model (prefixed by _ or smth). This is ofc dirty and will imply a performance penalty.

Actually, modifying the sub-model state is probably the only way - this is what gets compared as a result of mapState callback, and it needs to be different for re-render to happen. Another way would be changing the semantics somehow, but that’s even worse I think.

Fixing this will result in a pretty big performance hit for all consumers of easy-peasy (unless @ctrlplusb has an elegant way of solving this), and given that this seems to be a fairly uncommon issue - I think we're best of by leaving this as it is.

I have using easy-peasy quite extensively for years, and I haven't been affected by this or I have found a way to work around it.

There are ways around this for the consumer; one could use listeners to manually generate the "computed prop" - but I can see this not being an ideal solution.

We certainly have to update the docs though! Would you consider creating a PR to update the docs based on your findings?

yard commented 2 years ago

Sure thing – happy to update the docs. Shall I keep the current narrative of "avoid deconstructing the state to get computed properties" and augment the docs with an examples where it is legit?

jmyrland commented 2 years ago

Sure thing – happy to update the docs. Shall I keep the current narrative of "avoid deconstructing the state to get computed properties" and augment the docs with an examples where it is legit?

That would be awesome 👏

"Avoid destructing" in general might be on the extreme side, as it is only an issue whenever the computed prop is solely based on external state. "Be careful with computed props solely based on external state" might be a better approach.

We should also maybe offer a resolution to this:

Can this computed prop be moved it's dependent state?

const model = {
  session: {
    user: {
      current: null,
    },
    // ✅ isLoggedIn can be destructured safely in this case
    isLoggedIn: computed((user) => user.current != null),
  },

  other: {
    // ❌ isLoggedIn cannot be destructured safely in this case as it solely depends on external state
    // You need to target this property directly to ensure the latest value
    isLoggedIn: computed(
      [(_, storeState) => storeState.session.user],
      (user) => user.current != null
    ),
  },
};

Something like that?

yard commented 2 years ago

Yeah that makes sense, let me write it up :)

jmyrland commented 2 years ago

Thanks again!👏