Skip to content

Conversation

@tjenkinson
Copy link

@tjenkinson tjenkinson commented Apr 27, 2025

Thanks for the great library!

I wanted to show an error toast using toast.error() from inside a useLayoutEffect, but it didn't work.

This fixes that 🎉

So now providing the useLayoutEffect is from a component rendered after <Sonner /> it should work, and this also means that using useEffect anywhere should always work regardless of whether <Sonner /> is before or after the component using the hook.

Related #168

I also spotted a few race conditions so added some fixes for those in here too

I tried to run the tests locally but npm run dev doesn't seem to start the web server for some reason, not sure why

so that `useEffect` calls can emit toasts on the initial render
`useEffect` won't run the callback on the server, so it should always be there?
@vercel
Copy link

vercel bot commented Apr 27, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
sonner Ready Ready Preview, Comment Dec 18, 2025 11:03am

@@ -1,5 +1,7 @@
import React from 'react';

export const safeUseLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : () => {};
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neither useEffect or useLayoutEffect will run on the server, but useLayoutEffect will log a warning on the server, hence this

Comment on lines +194 to +201
React.useEffect(() => {
if (removed) {
const timeout = setTimeout(() => {
removeToastRef.current(toast);
}, TIME_BEFORE_UNMOUNT);
return () => clearTimeout(timeout);
}
}, [removed]);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved here so that the timer is cleared if component torn down

}
}

if (typeof window === 'undefined') return;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be needed given inside a useEffect, which won't run on the server?

@tjenkinson tjenkinson marked this pull request as ready for review April 27, 2025 16:02
@tjenkinson
Copy link
Author

Hey @emilkowalski any thoughts on this?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables toast notifications to work when triggered from useLayoutEffect by subscribing to state changes earlier in the component lifecycle. It also includes fixes for race conditions that could occur during component unmounting.

  • Introduces safeUseLayoutEffect to subscribe to toast state changes earlier in the render cycle
  • Refactors the Toast component's removal logic to use a ref pattern avoiding stale closures
  • Adds race condition guards using a tornDown flag to prevent state updates after unmounting

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/hooks.tsx Adds safeUseLayoutEffect utility that conditionally uses useLayoutEffect or a fallback based on environment
src/index.tsx Updates subscription logic to use safeUseLayoutEffect, refactors timeout handling with refs, adds race condition guards, and removes SSR window check

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -1,5 +1,7 @@
import React from 'react';

export const safeUseLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : () => {};
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safeUseLayoutEffect implementation should use React.useEffect as the fallback for server-side rendering, not an empty function. The current implementation returns an empty function that doesn't run at all, which means the subscription logic will never execute on the server and cleanup won't happen. This should be React.useEffect instead.

Suggested change
export const safeUseLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : () => {};
export const safeUseLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;

Copilot uses AI. Check for mistakes.
tornDown = true;
tearDown();
};
}, [toasts]);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency array includes toasts, which will cause the subscription to be recreated every time the toasts array changes. This defeats the purpose of using a subscription pattern and could lead to missed updates or duplicate subscriptions. The dependency array should be empty [] to ensure the subscription is only set up once on mount, similar to the useSonner hook at line 603.

Suggested change
}, [toasts]);
}, []);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant