Conversation
src/index.ts
Outdated
| const userHandleSet = options?.handleSet?.( | ||
| (store.temporal.getState() as _TemporalState<TState>) | ||
| ._handleSet as StoreApi<TState>['setState'], | ||
| ); | ||
|
|
||
| const internalHandleSet = ( | ||
| store.temporal.getState() as _TemporalState<TState> | ||
| )._handleSet; | ||
|
|
||
| const curriedHandleSet = userHandleSet || internalHandleSet; |
There was a problem hiding this comment.
For my understanding, these lines here are a more verbose way to write these four lines, correct?
const curriedHandleSet =
options?.handleSet?.(
(store.temporal.getState() as _TemporalState<TState>)._handleSet,
) || (store.temporal.getState() as _TemporalState<TState>)._handleSet;There was a problem hiding this comment.
If so, could we revert these changes to cut down on the number of lines changed? Thanks!
src/temporal.ts
Outdated
| _handleSet: (pastState, currentState, deltaState) => { | ||
| if (get().isTracking) { |
There was a problem hiding this comment.
Should this if statement also be moved into temporalHandleSet()?
There was a problem hiding this comment.
Taking a look at this now, I'm wondering if _handleSet should simply be removed from the temporal store because it's never externally referenced. What do you think?
There was a problem hiding this comment.
Re removing _handleSet -- my apologies, I am not totally confident I understand the potential refactor.
Although _handleSet shouldn't be accessed by the user (it is notably omitted from the TemporalState type, along with _onSave), _handleSet does still need access to get and set params passed by createStore to the stateCreator function returned by temporalStateCreator.
And then _handleSet is accessed in the curriedHandleSet expression via (store.temporal.getState() as _TemporalState<TState>)._handleSet;
I'm just trying to puzzle through how _handleSet might otherwise have access to get and set of the temporal store
EDIT: thinking more about this, I think I am understanding. We could remove it from the temporal store and just use store.temporal.getState() and store.temporal.setState
vaguely like:
// index.ts, below where we add `store.temporal = createStore...`
const handleSet = (pastState, currentState, deltaState) => {
if (store.temporal.getState().isTracking) {
// This naively assumes that only one new state can be added at a time
if (options?.limit && store.temporal.getState().pastStates.length >= options?.limit) {
store.temporal.getState().pastStates.shift();
}
store.temporal.getState()._onSave?.(pastState, currentState);
store.temporal.setState({
...store.temporal.getState(),
pastStates: get().pastStates.concat(deltaState || pastState),
futureStates: [],
});
}
}Is this what you had in mind?
There was a problem hiding this comment.
Re moving that if statement, yes that makes sense!
There was a problem hiding this comment.
Yes! Your code snippet is what I had in mind; however, this is simply a hunch that it will work but I haven't tested it.
We could leave it as a follow-up PR to avoid many changes in a single PR. What do you think about you or I making this change after this current PR is merged?
There was a problem hiding this comment.
Yeah that sounds good! Happy to make a follow up PR after this one to test it out.
src/types.ts
Outdated
| ) => ( | ||
| pastState: PartialTState, | ||
| currentState: PartialTState, | ||
| deltaState?: Partial<PartialTState> | null, | ||
| ) => void; |
There was a problem hiding this comment.
My only concern here is that this is technically a breaking type change. Have you considered (this mess of a) type signature to avoid breaking type changes?
| ) => ( | |
| pastState: PartialTState, | |
| currentState: PartialTState, | |
| deltaState?: Partial<PartialTState> | null, | |
| ) => void; | |
| ) => ( | |
| partial: Parameters<StoreApi<TState>['setState']>[0], // or maybe PartialTState | |
| replace: Parameters<StoreApi<TState>['setState']>[1], // or maybe boolean | undefined | |
| currentState: PartialTState, | |
| deltaState?: Partial<PartialTState> | null, | |
| ) => void; |
There was a problem hiding this comment.
I had not previously considered this, but if it is okay with you then it is okay with me! Very cleverly avoids the breaking change!
I doubt that users will ever want to access the value of replace in the function returned by their handleSet function, but they already can as is. I will update to reflect this on my next push. I'll plan on passing undefined as the value for "replace" given to curriedHandleSet, which is what the value is now.
There was a problem hiding this comment.
Let's plan on this to avoid a breaking change. Can you please add a comment in the future replace will likely be deprecated and removed? Thanks!
charkour
left a comment
There was a problem hiding this comment.
Thanks for the very detailed explanation and writeup! Just a couple of small comments for you to address before we merge this in. Out of all three options, I'm leaning toward this one. Based on your use case, does this one make sense to you, @pstrassmann? Thank you
|
@charkour Hey! Sorry I will get to all of this this week! Thank you! |
|
No rush. Happy new year! |
| ### Cool-off period | ||
|
|
||
| `handleSet?: (handleSet: StoreApi<TState>['setState']) => StoreApi<TState>['setState']` | ||
| ```typescript | ||
| handleSet?: (handleSet: StoreApi<TState>['setState']) => ( | ||
| pastState: Parameters<StoreApi<TState>['setState']>[0], | ||
| // `replace` will likely be deprecated and removed in the future | ||
| replace: Parameters<StoreApi<TState>['setState']>[1], | ||
| currentState: PartialTState, | ||
| deltaState?: Partial<PartialTState> | null, | ||
| ) => void | ||
| ``` |
There was a problem hiding this comment.
I have some concerns that this type definition may spook new users reading the README, but it also seems like a good idea to be accurate here?
There was a problem hiding this comment.
I'm wondering if there are easier ways to write this out. If not, it is what it is and we can simplify this in v3.
There was a problem hiding this comment.
I'm not sure. The use of the Parameters utility type is actually easier on the eyes to me than directly using/copying/extending the types from SetStateInternal from zustand:
type SetStateInternal<T> = {
_(partial: T | Partial<T> | {
_(state: T): T | Partial<T>;
}['_'], replace?: boolean | undefined): void;
}['_'];I think the addition in this PR just looks ugly (in part?) because, whereas previously replace was hidden within StoreApi<TState>['setState'], we make it visible here in order to add the two additional parameters currentState and deltaState
There was a problem hiding this comment.
I agree. Let's merge this in as is, but I'll make a follow-up PR with some options.
|
@charkour Yes this PR I think is the best of all the options. Happy to close out the other ones and merge this one in. Very happy we were able to land on a solution without any breaking changes to the API! Thanks again for all your help on this, and I'm looking forward to helping grow and improve Happy to address any additional feedback on this PR as well. Thank you! |
|
Thanks for the awesome work! I'm glad that zundo is very helpful when adding new features to your platform at work. I plan to review this soon and would like to release a new version this weekend. Sorry for the delay on this! |
|
Thank you!! 🙏 |
| options?.handleSet?.( | ||
| (store.temporal.getState() as _TemporalState<TState>)._handleSet, | ||
| (store.temporal.getState() as _TemporalState<TState>) | ||
| ._handleSet as StoreApi<TState>['setState'], |
There was a problem hiding this comment.
Why is this typecase needed? Is that because this function technically takes four parameters including currentState and deltaState?
There was a problem hiding this comment.
Yeah that is right. I believe it is required due to the fact that curriedHandleSet is used to call either the internal handle set or the user-provided handleSet.
Perhaps this is a circular explanation, but options.handleSet takes an argument of type StoreApi<TState>['setState'].
However, this is not the type of _handleSet, which takes four arguments. So asserting it to the be the type expected by options.handleSet resolves the type error.
I'll be the first to admit that this part of the code is not the easiest to read/follow. I don't immediately have an alternative suggestion, but I would certainly be open to something more approachable in upcoming versions.
charkour
left a comment
There was a problem hiding this comment.
Thanks for the amazing work on this! Before we release a new version, I'll plan on you removing _handleSet from the internal zustand store and I'll work on updating some types.
Thank you!
|
@charkour Ok sounds good! I'll find some time in the next couple days to get the followup PR in. Thank you! |
|
Looking more at the types, I think this is as good as we are going to get it without breaking API changes. I'll work on a v3 branch with simplification. |
This PR is to explore the solution space around ideas around this issue, specifically based on this comment
TL;DR This PR and PR 141 look very similar now at first glance. To see the primary difference, note how
curriedHandleSettakes different numbers of parameters in the two PRs.Background:
In PR 141, there are zero changes to the ZundoOptions API. However, in bringing the state change check in prior to calling
curriedHandleSet, we have the minorish drawback of having to calculatecurrentStateanddeltaStateBOTH 1) to calculate the check for callingcurriedHandleSetand 2) withincurriedHandleSet.An idea was then -- instead of calculating those values twice, maybe we just calculate those things once and just pass them to
curriedHandleSet. So that's what we do in this PR.While we resolve the redundant calculation issue, the drawback though is that we cannot both let
curriedHandleSetaccept more parameters and not simultaneously have absolutely zero changes to the API. Like PR 142, options.handleSet now returns a function that, in addition topastStateas before, now technically also receives the values ofcurrentStateanddeltaState. So like in PR 142, the minor change to the API is that users now also have access to these values and can use them in logic (although in this PR they no longer need to to resolve the throttling/untrackedValue issues themselves with additional logic, as that is now, like in PR 141, always resolved by the conditional check if equality or diff functions exist.All that being said, I believe this change (this PR) is non breaking and all current compositions of
handleSetwill continue to work as expected (the same is true of the other PRs as well). All tests pass, and this was tested locally with previous compositions ofhandleSet.