[toast] Add useToastActions hook to avoid unnecessary re-renders#4233
[toast] Add useToastActions hook to avoid unnecessary re-renders#4233Yevgeni-Esh wants to merge 1 commit intomui:masterfrom
Conversation
commit: |
Bundle size report
Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
b819a1a to
4776452
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4776452 to
4e45a5f
Compare
| ## useToastActions | ||
|
|
||
| Returns only the action methods without subscribing to toast state. | ||
| Use this hook in components that only need to **fire** toasts (for example, buttons) but don't need to read the `toasts` array. | ||
| Unlike `useToastManager`, this hook will not cause the component to re-render when toasts are added, updated, or removed. |
There was a problem hiding this comment.
This looks okay, but you can also use the global manager to avoid re-renders, and might be better than adding a new API to the mix here
There was a problem hiding this comment.
I think using just global manager is not ideal
in a nested page or component causing re-renders... i don't think its best practice to use the ref of the global manager object
re-render example
https://stackblitz.com/edit/9xjrqjfk-mu5uzrrf?file=src%2FApp.tsx
using const { add } = Toast.useToastManager();
like it says to use in the documentation.
no rerender
https://stackblitz.com/edit/9xjrqjfk?file=src%2FApp.tsx
using toastManager.add({
but this means you must make an export of export const toastManager = Toast.createToastManager(); in the main app and import it from a page/component and not use it from the provider
but i get that this involves new API...
or... maybe just update the docs that doing const { add } = Toast.useToastManager(); may cause re-renders so they should do import toastManager and do toastManager.add? :)
Fixes #4234
Problem
useToastManager()unconditionally callsstore.useState('toasts'), which registers auseSyncExternalStoresubscription. This means every component that callsuseToastManager()re-renders on every toast lifecycle event (add, close, update, timeout) — even if the component only destructures action methods like{ add }and never reads thetoastsarray.The action methods (
addToast,closeToast, etc.) are stable references — arrow functions on theToastStoreclass instance. But the subscription is registered before the caller destructures the return value, so destructuring only{ add }doesn't help.Solution
Add a new
useToastActions()hook that returns only the stable action methods (add,close,update,promise) without subscribing to state:store.useState()call → nouseSyncExternalStoresubscription → zero re-renders from toast state changesToastContextholds theToastStoreinstance (stable ref created viauseRefWithInit), souseContextwon't trigger re-renders eitherUsage
Changes
packages/react/src/toast/useToastActions.tsindex.parts.tsandindex.tsuseToastManager.spec.tsx— validates generic<Data>inferenceuseToastActionssection to toast docs with return value reference and usage guidancetest/public-types/toast.tsxwithUseToastActionsReturnValueAll existing
useToastManagertests (31/31) continue to pass.