Immer: It it possible to access the "finished" object in the current state from within `produce`?

Created on 28 Mar 2020  ·  5Comments  ·  Source: immerjs/immer

🙋‍♂ Question

Heya. We just had an interesting bug report over at @reduxjs/toolkit that is intertwined with immer, so I'd love to hear your opinion on this - maybe you have a suggestion.

We were presented with this code:

const adapter = createEntityAdapter();
const selectors = adapter.getSelectors(state => state.some);

...
initialState: adapter.getInitialState(),
reducers: {
    remove(state, action) {
       console.log(selectors.selectIds(state).length); // let's say it's 10
       adapter.removeMany(state, action.payload.ids); // ids = [1, 2, 3]
       console.log(selectors.selectIds(state).length); // I would expect 7, but it's still 10 !
    },
...  

The point here is that selectors.selectIds are reselect selectors, which are memoized functions. If they are called with the same state, they will return the same value.
This whole remove function is being called within a produce call, so state will in both calls be the same Proxy object - so calling the selector after modifying something in state, will still return the same value.

We can override the equalityCheck function of reselect, but the question is now how to override it best.

Doing something like

function defaultEqualityCheck(currentVal, previousVal) {
  return currentVal === previousVal && !isDraft(currentVal)
}

will get us about 80% of the way, as it would assume that drafted values are always different to each other.

But there would be cases where these values should be equal and no new return value should be produced - after all, the memoization of reselect is also used to provide stable object identity on the return value in case the state wasn't changed; which would not be a given any more if we were to take this approach.

So the best thing would be if we could peek at the value to see if it (or one of it's children) have changed between two snapshots of the proxy. Is there any way to do that? (Or another way, like actually accessing the patches while they are being generated?)

question

Most helpful comment

This is a nice example why drafts are not allowed to be leaked from producers. Using a draft as memoization key is an example of that indeed :) My gut feeling for a solution would be to be able to separate the process of selecting from the process of memoization. There is little point in memoizing a draft; it will disappear very soonish, and reading from it in the future will actually thrown an error. Is there a way you can call the selector with triggering the memoization?

Otherwise the isDraft check looks correct to me.

But there would be cases where these values should be equal and no new return value should be produced

I didn't get entirely what you mean by that, stable object identity has only meaning outside the concepts of drafts / producers. Can you give an example where that would be an issue?

So the best thing would be if we could peek

In the ES5 implementation we only compute differences at the end, so we can't tell that reliably inside the producer (without making stuff really expensive at least). Patches are only generated at the end of the process as well.

@markerikson I'm not sure how that is a related question :-P. But a simple utility that clones before logging would work (e.g.console.log(JSON.parse(JSON.stringify(draft))). Clearly that is expensive but not sure if that is a problem during debugging. But for further discussion please open a separate question :). I hope this gets fixed at some point at the browser level, as to me it doesn't make sense that proxies are transparent everywhere, except during logging :-P.

All 5 comments

Closely related question:

There are times when I might want to console.log(draft) to see what the values look like, but because it's a Proxy, you can't see inside.

What's the right solution for that use case?

This is a nice example why drafts are not allowed to be leaked from producers. Using a draft as memoization key is an example of that indeed :) My gut feeling for a solution would be to be able to separate the process of selecting from the process of memoization. There is little point in memoizing a draft; it will disappear very soonish, and reading from it in the future will actually thrown an error. Is there a way you can call the selector with triggering the memoization?

Otherwise the isDraft check looks correct to me.

But there would be cases where these values should be equal and no new return value should be produced

I didn't get entirely what you mean by that, stable object identity has only meaning outside the concepts of drafts / producers. Can you give an example where that would be an issue?

So the best thing would be if we could peek

In the ES5 implementation we only compute differences at the end, so we can't tell that reliably inside the producer (without making stuff really expensive at least). Patches are only generated at the end of the process as well.

@markerikson I'm not sure how that is a related question :-P. But a simple utility that clones before logging would work (e.g.console.log(JSON.parse(JSON.stringify(draft))). Clearly that is expensive but not sure if that is a problem during debugging. But for further discussion please open a separate question :). I hope this gets fixed at some point at the browser level, as to me it doesn't make sense that proxies are transparent everywhere, except during logging :-P.

Is there a way you can call the selector with triggering the memoization?

Essentially, using the selectors within the produce call is nothing we'd ever come up with.
It's just that we are exposing selectors and a user started using them in a caseReducer - which gets wrapped. So we're trying to handle user curiosity as best as we can I guess :D

But there would be cases where these values should be equal and no new return value should be produced

I didn't get entirely what you mean by that, stable object identity has only meaning outside the concepts of drafts / producers. Can you give an example where that would be an issue?

Going for a bit of a fabricated non-sense example here - it didn't occur to me in the first place to use selectors within the reducer, so I cannot really think of anything feasible right now ;)

function caseReducer(draft, action) {
  draft.firstName = action.payload;
  draft.address = selectFullAddress(draft);
}

So here it would be great if draft.address could keep a stable object identity compared to the last reducer execution, assuming firstName didn't change.

Again, not a very good example, but trying to come up with something simple that somehow showcases the problem here. I certainly wouldn't use it like that :D

So the best thing would be if we could peek

In the ES5 implementation we only compute differences at the end, so we can't tell that reliably inside the producer (without making stuff really expensive at least). Patches are only generated at the end of the process as well.

Well, if that isn't possible I guess there's nothing we really can do. Guess we might consider to go with complete non-memoization in the draft case then. We'll have to discuss that :)

Yes when finalizing you will get the old ref back in such cases if the
selected data didnt change, no need to do anything special here, unless the
selector itself creates fresh objects.

Op di 31 mrt. 2020 23:50 schreef Lenz Weber notifications@github.com:

But there would be cases where these values should be equal and no new
return value should be produced

I didn't get entirely what you mean by that, stable object identity has
only meaning outside the concepts of drafts / producers. Can you give an
example where that would be an issue?

Going for a bit of a fabricated non-sense example here - it didn't occur
to me in the first place to use selectors within the reducer, so I cannot
really think of anything feasible right now ;)

function caseReducer(draft, action) {
draft.firstName = action.payload;
draft.address = selectFullAddress(draft);
}

So here it would be great if draft.address could keep a stable object
identity compared to the last reducer execution, assuming firstName
didn't change.

Again, not a very good example, but trying to come up with something
simple that somehow showcases the problem here. I certainly wouldn't
use it like that :D


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/immerjs/immer/issues/561#issuecomment-606925600, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN4NBDOF5V4HIH627OBX2DRKJXSPANCNFSM4LVSNZEA
.

Problem is - many selectors generate fresh object. But I guess I'm pre-anticipating problems that nobody might ever run into :)

So I'll close this for now - thanks for the insight into the internals!

Was this page helpful?
0 / 5 - 0 ratings