Skip to content

[toast] Add useToastActions hook to avoid unnecessary re-renders#4233

Open
Yevgeni-Esh wants to merge 1 commit intomui:masterfrom
Yevgeni-Esh:fix/toast-useToastActions
Open

[toast] Add useToastActions hook to avoid unnecessary re-renders#4233
Yevgeni-Esh wants to merge 1 commit intomui:masterfrom
Yevgeni-Esh:fix/toast-useToastActions

Conversation

@Yevgeni-Esh
Copy link

@Yevgeni-Esh Yevgeni-Esh commented Mar 2, 2026

Fixes #4234

Problem

useToastManager() unconditionally calls store.useState('toasts'), which registers a useSyncExternalStore subscription. This means every component that calls useToastManager() re-renders on every toast lifecycle event (add, close, update, timeout) — even if the component only destructures action methods like { add } and never reads the toasts array.

// This button re-renders on every toast state change, despite only using `add`
function SaveButton() {
  const { add } = Toast.useToastManager();
  return <button onClick={() => add({ title: 'Saved!' })}>Save</button>;
}

The action methods (addToast, closeToast, etc.) are stable references — arrow functions on the ToastStore class 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:

  • No store.useState() call → no useSyncExternalStore subscription → zero re-renders from toast state changes
  • ToastContext holds the ToastStore instance (stable ref created via useRefWithInit), so useContext won't trigger re-renders either
  • The action methods are arrow functions on the class instance — their references are stable

Usage

// Components that only FIRE toasts — zero re-renders from toast state
function SaveButton() {
  const { add } = Toast.useToastActions();
  return <button onClick={() => add({ title: 'Saved!' })}>Save</button>;
}

// Components that DISPLAY toasts — subscribe to state (unchanged)
function ToastList() {
  const { toasts } = Toast.useToastManager();
  return toasts.map((toast) => <Toast.Root key={toast.id} toast={toast} />);
}

Changes

  • New hook: packages/react/src/toast/useToastActions.ts
  • Exports: Added to index.parts.ts and index.ts
  • Tests: 5 new tests including a render count assertion that verifies the component does not re-render when toasts are added/dismissed
  • Type spec: Mirrors useToastManager.spec.tsx — validates generic <Data> inference
  • Docs: Added useToastActions section to toast docs with return value reference and usage guidance
  • Public types: Updated test/public-types/toast.tsx with UseToastActionsReturnValue
  • Error codes: New code 93 for missing provider error

All existing useToastManager tests (31/31) continue to pass.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 2, 2026

commit: 0af6640

@mui-bot
Copy link

mui-bot commented Mar 2, 2026

Bundle size report

Bundle Parsed size Gzip size
@base-ui/react 🔺+196B(+0.04%) 🔺+29B(+0.02%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link

netlify bot commented Mar 2, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 4e45a5f
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/69a55733b9ac160008e3fd34
😎 Deploy Preview https://deploy-preview-4233--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Yevgeni-Esh Yevgeni-Esh force-pushed the fix/toast-useToastActions branch from b819a1a to 4776452 Compare March 2, 2026 09:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Yevgeni-Esh Yevgeni-Esh force-pushed the fix/toast-useToastActions branch from 4776452 to 4e45a5f Compare March 2, 2026 09:24
@zannager zannager added the component: toast Changes related to the toast component. label Mar 2, 2026
Comment on lines +448 to +452
## 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Author

@Yevgeni-Esh Yevgeni-Esh Mar 3, 2026

Choose a reason for hiding this comment

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

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? :)

Copy link
Author

Choose a reason for hiding this comment

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

@atomiks
WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: toast Changes related to the toast component.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[toast] useToastManager() causes unnecessary re-renders for action-only consumers

4 participants