-
-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: don't call handleSet if state hasn't changed #141
fix: don't call handleSet if state hasn't changed #141
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. |
@@ -717,6 +727,184 @@ describe('Middleware options', () => { | |||
); | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make that change here.
src/index.ts
Outdated
@@ -72,7 +72,21 @@ export const temporal = (<TState>( | |||
// The order of the get() and set() calls is important here. | |||
const pastState = options?.partialize?.(get()) || get(); | |||
set(...args); | |||
curriedHandleSet(pastState); | |||
const currentState = options?.partialize?.(get()) || get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beautiful, thank you!
Closing in favor of #149 |
Currently, handleSet gets called even if temporal
state
hasn'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
pastState
andcurrentState
are 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: