-
-
Notifications
You must be signed in to change notification settings - Fork 298
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 setState race conditions #189
Fix setState race conditions #189
Conversation
Well done @simonbos 👏🏼👏🏼 One of the best PRs from the community here. Changes:
Let me know what you think about the changes I made. |
Thanks for the updates, looks very good in general! Some remarks:
I would opt for the second option (e.g for a Loading Snackbar), but we could also support both use cases (e.g. with a |
Haven't tested this but I'd propose: handleCloseSnack = (event, reason, key) => {
// ...
if (reason === REASONS.CLICKAWAY) return;
const shouldCloseAll = key === undefined;
this.setState(({ snacks, queue }) => ({
snacks: snacks
.filter(item => (
// calling close on a snackbar that hasn't entered the screen yet.
// i.e. call closeSnackbar immediately after enqueueSnackbar.
(shouldCloseAll || item.key === key) && !item.entered
))
.map(item => (
(shouldCloseAll || item.key === key) ? { ...item, open: false } : { ...item }
)),
queue: queue.filter(item => item.key !== key), // eslint-disable-line react/no-unused-state
}));
}; Lmk what you think. @simonbos |
Ok for 1-3! I agree that no extra props should be added for a feature of which we aren't sure if it would be used 😉 If such a feature would be implemented in the future, I would go for adding a parameter to the function, i.e. something in the style of Your code looks good; the only thing that should be added is that if an element is effectively filtered in the Adapted code (unused and unoptimal!!): handleCloseSnack = (event, reason, key) => {
// ...
if (reason === REASONS.CLICKAWAY) return;
this.setState((state) => {
// remove all messages if no key specified
if (key === undefined)
return {...state, queue: [], snacks: []};
// delete specific snack from queue
const newQueue = state.queue.filter(item => item.key !== key); // eslint-disable-line react/no-unused-state
// delete specific snack if not entered
if (state.snacks.some((item) => item.key === key && !item.entered)) {
const newState = this.processQueue({
...state,
snacks: state.snacks.filter(item => item.key !== key),
queue: newQueue,
});
return newState.queue.length === 0
? newState
: this.handleDismissOldest(newState);
}
// close specific snack if entered
return {
...state,
snacks: state.snacks.map(item => (
(item.key === key) ? { ...item, open: false } : { ...item }
)),
queue: newQueue,
}
});
} However, as you can see in the following codesandbox, this code is not optimal (click default): https://codesandbox.io/s/notistack-simple-example-8mlvg The issue appears because the entered state is applied only once the transition is fully over. This issue could be solved by also passing the closeSnackbar = (key, immediately=true) => {
if (immediately)
this.handleExitedSnack(null, key)
else
this.handleCloseSnack(null, null, key);
} If we do it like this, the responsibilities of the functions are nicely divided: Any last remarks, @iamhosseindhv ? |
Perfect. I think everyone using notistack, specially myself, appreciate your great work. Happy to merge? |
@iamhosseindhv It's my pleasure, thank you for the nice package! Merge 👍 |
This pull request fixes #38 and #175. Main rationale of the pull request:
handleDismissOldest
no longer removes closed messages, this should only happen inhandleExitedSnack
.entered
andrequestClose
boolean variables. With these variables, a snackbar should first have fully entered the window before it can be dismissed. This is needed for e.g. the casemaxSnack = 1
andenqueueSnackbar
called twice. Without the variables, theopen
prop of the first snackbar would immediately be set tofalse
, without the snackbar ever being rendered. In this case, thehandleExitedSnack
function would never be called for that snackbar and it would be always in the view (although hidden due toopen = false
).queue: queue.filter(item => item.key !== key),
tohandleCloseSnack
for Persistent snackbar not (always) closing with closeSnackbar(key) #175.Old codesandbox (#38): https://codesandbox.io/s/notistack-simple-example-xpff1
Fixed codesandbox: https://codesandbox.io/s/notistack-simple-example-yd6gw
Note: a possible 'breaking' change is that
enqueueSnackbar
no longer returns null as id if it prevented a duplicate. (This is due to the asynchronous setState.)Any comments are welcome.