fix: don't call handleSet if state hasn't changed#141
fix: don't call handleSet if state hasn't changed#141pstrassmann wants to merge 4 commits intocharkour:mainfrom
Conversation
|
Hi @pstrassmann, thanks for the PR! Could you write tests that fail but pass with your fix? I realize the bug is due to the |
|
@charkour Yes, will do. I am also concerned with causing regressions or inadvertently introducing a breaking change. I also have one other idea that I’ll write write up tomorrow that may be more in the zundo spirit of leaving implantation to the user. The basic idea would be to allow the user access ‘previousState’ and ‘currentState’ inside the callback that consumers pass to the ‘handleState’ ‘ZundoOption’. With access to previous and current state, they could themselves then write the conditional that determines whether ‘handleState’ is run. But there may be snags I haven’t thought of, but I will keep chewing on it. |
| expect(console.warn).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it('should not call throttle function if partialized state is unchanged according to equality fn', () => { |
There was a problem hiding this comment.
This test fails on main branch
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it('should not call throttle function if partialized state is unchanged according to diff fn', () => { |
There was a problem hiding this comment.
This test fails on main branch
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it('should always call throttle function on any partialized or non-partialized state change if no equality or diff fn is provided', () => { |
There was a problem hiding this comment.
This test passes on main branch, but seemed helpful to explicitly define and to call out.
See this gif for what this looks like (notice useless history):
src/index.ts
Outdated
| ) | ||
| ) | ||
| ) { | ||
| curriedHandleSet(pastState); |
There was a problem hiding this comment.
If we go forward with this, what do you think about passing currentState and deltaState to the curriedHandleSet?
| curriedHandleSet(pastState); | |
| curriedHandleSet(pastState, currentState, deltaState); |
There was a problem hiding this comment.
Yeah this makes sense!
There was a problem hiding this comment.
@charkour Actually I think we may run into some type issues doing this if we want to use a curriedHandleSet rather than something more like in PR: 142. I'm not positive and am trying to troubleshoot this now.
Expanding handleSet to expect additional params currentState and deltaState is currently causing some tangles, where we would need to know currentState and deltaState when we define curriedHandleState, which I'm not sure is possible (since it is defined outside of any function that sets zustand state so that currentState becomes a thing)
I'll keep thinking about it.
There was a problem hiding this comment.
Ah, that is tricky, maybe this way isn't doable without a major version change. Could you see what's possible to do without major version changes and then we could consider a future API for v3?
There was a problem hiding this comment.
Yeah for sure, I'll keep noodling on this and see if I can find a solution that is more of something in between PR 141 and 142, biasing toward something that doesn't change the API
There was a problem hiding this comment.
Sounds good! Thanks for the help on this!
| if (get().isTracking) { | ||
| const currentState = options?.partialize?.(userGet()) || userGet(); | ||
| const deltaState = options?.diff?.(pastState, currentState); |
There was a problem hiding this comment.
Could you move these lines into the src/index.ts file?
src/index.ts
Outdated
| options?.equality?.(pastState, currentState) || | ||
| // If the user has provided a diff function but nothing has been changed, function returns null | ||
| deltaState === null |
There was a problem hiding this comment.
Looking back at the old code, an improved heuristic would be to first check deltaState === null before checking equality because that could be a potentially expensive operation.
src/index.ts
Outdated
| const pastState = options?.partialize?.(get()) || get(); | ||
| set(...args); | ||
| curriedHandleSet(pastState); | ||
| const currentState = options?.partialize?.(get()) || get(); |
There was a problem hiding this comment.
For consistency, could you also make sure this change takes place in the setState function on line 58? Thanks!
There was a problem hiding this comment.
Yeah can do. To be honest I haven't quite been able to wrap my head around what is going on on line 60. Do you happen to have a quick way of explaining what is going on there? 😅
There was a problem hiding this comment.
Sure! The quick explanation is that zustand has two ways of setting state; one is during the creation method where set is used within the callback (used often) and the second is after the store is created, calling myStore.setState (which is less used):
const myStore = create((set) => ({ increment: set((state) => ({ bears: state.bears + 1 })});
myStore.setState({ bears: 99 });The zundo user might prefer one setter over the other, but to make sure the middleware acts the same, we need to modify both setters. This was really brief, so I'm happy to elaborate more.
There was a problem hiding this comment.
The set version is updated to track history within the config function call, and the setState version is directly mutated on the user's store object (the object returned by zustand/create or zustand/createStore).
There was a problem hiding this comment.
Beautiful, thank you!
|
Closing in favor of #149 |

Currently, handleSet gets called even if temporal
statehasn't changed. One negative result of this is that throttles or debouncers to tracking history can get initialized even if no history-tracked state changes. This can result in unexpected behavior, such as the first instance of a history tracked state changing not getting registered in history, so long as it was preceded by a non-history tracked state changing.This PR implements a change so that handleState is only called if
pastStateandcurrentStateare not equal.I'd appreciate some review and feedback, as I am new to this repo and want to make sure that I'm handling all edge cases correctly.
This PR has been tested locally against code like in this Sandbox:
https://codesandbox.io/p/sandbox/zundo-untrackedvalue-change-initiating-throttle-issue-wq9cm8?file=%2Fsrc%2FApp.tsx%3A58%2C11
Here is a gif of this fix working:
