Skip to content

fix(react): support pre-mount toast calls#1217

Merged
georgewrmarshall merged 2 commits into
mainfrom
toast-follow-ups
Jun 8, 2026
Merged

fix(react): support pre-mount toast calls#1217
georgewrmarshall merged 2 commits into
mainfrom
toast-follow-ups

Conversation

@georgewrmarshall

@georgewrmarshall georgewrmarshall commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Description

This PR updates the React Toast imperative API so toast(...) and toast.dismiss() work before <Toaster /> mounts.

The previous implementation registered the mounted Toaster ref during render/effects and threw when the static API was called before mount. This moves the static API behind a small module-level single-toast store. <Toaster /> subscribes to that store, renders any pending toast on mount, and keeps the existing single-toast replacement behavior.

Also included:

  • Preserve the Toast cleanup by passing onClose={onClose} directly.
  • Update Toast README language for pre-mount toast(...) and toast.dismiss().
  • Add tests for pre-mount calls, latest-toast wins behavior, pre-mount dismiss, mounted dismiss, unmount/remount store behavior, and the shared store-backed ref/static API path.

Related issues

Fixes:

Manual testing steps

  1. Run cd packages/design-system-react.
  2. Run yarn test --runInBand --runTestsByPath src/components/Toast/Toast.test.tsx src/components/Toast/Toaster.test.tsx --coverage=false.
  3. Confirm the Toast and Toaster test suites pass.

Screenshots/Recordings

N/A. This is a behavior and test coverage follow-up for the Toast API.

Before

N/A

After

N/A

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I’ve included tests if applicable
  • I’ve documented my code using JSDoc format if applicable
  • I’ve applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Changes global toast state and mount-order behavior across apps that call toast() early; limited to design-system Toast but affects any consumer timing assumptions.

Overview
The React toast() / toast.dismiss() API no longer requires a mounted <Toaster />. Imperative calls are backed by a module-level single-toast store that <Toaster /> subscribes to (via useLayoutEffect), so toasts queued before mount show after mount, the latest call wins, and pre-mount dismiss clears pending state. Unmounting <Toaster /> no longer unregisters the API—calls while unmounted are held until the next mount.

<Toast /> passes onClose through to BannerBase directly instead of wrapping it. Docs and tests were updated for the new behavior (pre-mount, dismiss, unmount/remount); tests that expected throws before mount were removed.

Reviewed by Cursor Bugbot for commit c8724e8. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

}
return registeredRef.current;
let storeToastOptions: ToastOptions | undefined;
const toastStoreListeners = new Set<ToastStoreListener>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reference implementation: react-hot-toast keeps toast state in a module-level store with listeners while Toaster consumes that store; this PR keeps the same store/listener shape but intentionally remains single-slot. Relevant files: https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts and https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx

useImperativeHandle(ref, () => innerRef.current as ToasterRef);
useImperativeHandle(
ref,
() => ({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ref methods now write through the store instead of assigning an imperative object during render, which addresses the React compiler concern from PR #1190: #1190 (comment)

jest.useRealTimers();
});

it('displays toast called before <Toaster /> mounts', async () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These tests replace the previous documented throw behavior with the react-hot-toast-style pre-mount store behavior; the old self-review reference is here: #1190 (comment)

className={twMerge('rounded-xl', className)}
closeButtonProps={resolvedCloseButtonProps}
onClose={onClose ? () => onClose() : undefined}
onClose={onClose}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This keeps the cleanup requested on the previous Toast PR: onClose is already function or undefined, so wrapping it only adds an unnecessary render-time closure. Previous discussion: #1190 (comment)

@georgewrmarshall

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@georgewrmarshall georgewrmarshall self-assigned this Jun 5, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/design-system-react": "0.25.0-preview.1f40f69f",
  "@metamask-previews/design-system-react-native": "0.28.0-preview.1f40f69f",
  "@metamask-previews/design-system-shared": "0.21.0-preview.1f40f69f",
  "@metamask-previews/design-system-tailwind-preset": "0.9.0-preview.1f40f69f",
  "@metamask-previews/design-system-twrnc-preset": "0.5.0-preview.1f40f69f",
  "@metamask-previews/design-tokens": "8.5.0-preview.1f40f69f"
}

@georgewrmarshall georgewrmarshall marked this pull request as ready for review June 8, 2026 17:08
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner June 8, 2026 17:08
@georgewrmarshall georgewrmarshall enabled auto-merge (squash) June 8, 2026 17:08
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall merged commit a635705 into main Jun 8, 2026
36 checks passed
@georgewrmarshall georgewrmarshall deleted the toast-follow-ups branch June 8, 2026 17:12
@georgewrmarshall georgewrmarshall mentioned this pull request Jun 9, 2026
18 tasks
georgewrmarshall added a commit that referenced this pull request Jun 9, 2026
## Release 44.0.0

This release adds `Toast`/`Toaster` and `Tag` to React, `ListItem` to
React Native, aligns the `TextButton` API across platforms, and
standardizes severity vocabulary (`Error` → `Danger`) across
`AvatarIcon`, `IconAlert`, and `Tag`.

### 📦 Package Versions

- `@metamask/design-system-shared`: **0.22.0**
- `@metamask/design-system-react`: **0.26.0**
- `@metamask/design-system-react-native`: **0.29.0**

### 🔄 Shared Type Updates (0.22.0)

#### New shared types (#1190, #1203, #1224, #1225)

**What Changed:**

- Added `ToastPropsShared` and `ToastSeverity` for cross-platform
`Toast` support
- Added `ListItemPropsShared` and related types for cross-platform
`ListItem` support
- Added `TextButtonPropsShared` to align `TextButton` API across React
and React Native
- Added `AvatarNetworkSize` as a named export from the shared package

**Impact:**

- Enables consistent `Toast` and `ListItem` implementations across both
platforms
- Continues ADR-0003/0004 const-object + string-union pattern adoption

#### Severity vocabulary: `.Error` → `.Danger`
([#1159](#1159))

**What Changed:**

- `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger`
- `IconAlertSeverity.Error` → `IconAlertSeverity.Danger`
- `TagSeverity.Error` → `TagSeverity.Danger`

**Impact:**

- Breaking change for any consumer using `.Error` on these three const
objects. Rendered colors are unchanged.

### 🌐 React Web Updates (0.26.0)

#### Added

- Added `Tag` component for categorization and filtering labels
([#1211](#1211))
- Added `Toast` component with `Toaster` provider and imperative
`toast()` API
([#1190](#1190))

#### Changed

- **BREAKING:** `TextButton` API aligned with React Native —
`size`/`TextButtonSize` replaced by `variant`/`TextVariant`;
`isInverse`, `isDisabled`, `textProps`, and icon/accessory props
removed; `asChild` added
([#1224](#1224))
- **BREAKING:** `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger`
([#1159](#1159))

#### Fixed

- Fixed `Toast` to support `toast()` calls made before `Toaster` mounts
([#1217](#1217))

### 📱 React Native Updates (0.29.0)

#### Added

- Added `ListItem` component for list row layouts
([#1203](#1203))
- Added `Toast` component with `Toaster` provider and imperative
`toast()` API
([#1190](#1190))

#### Changed

- **BREAKING:** `AvatarIconSeverity.Error`, `IconAlertSeverity.Error`,
and `TagSeverity.Error` → `.Danger`
([#1159](#1159))

### ⚠️ Breaking Changes

#### TextButton rewrite (React Web Only)

**What Changed:**

- `size` / `TextButtonSize` removed — use `variant` / `TextVariant`
instead
- `isInverse`, `isDisabled`, `textProps`, start/end icons, and accessory
slots removed
- `asChild` added for semantic link composition

**Migration:**

```tsx
// Before (0.25.0)
import { TextButton, TextButtonSize } from '@metamask/design-system-react';
<TextButton size={TextButtonSize.BodySm}>Learn more</TextButton>

// After (0.26.0)
import { TextButton } from '@metamask/design-system-react';
import { TextVariant } from '@metamask/design-system-shared';
<TextButton variant={TextVariant.BodySm}>Learn more</TextButton>
```

See [React Migration
Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260)

#### Severity vocabulary: `.Error` → `.Danger` (Both Platforms)

**What Changed:**

- `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger` (React +
React Native)
- `IconAlertSeverity.Error` → `IconAlertSeverity.Danger` (React Native)
- `TagSeverity.Error` → `TagSeverity.Danger` (React Native)

**Migration:**

```tsx
// Before
<AvatarIcon severity={AvatarIconSeverity.Error} />
<IconAlert severity={IconAlertSeverity.Error} />
<Tag severity={TagSeverity.Error}>High risk</Tag>

// After
<AvatarIcon severity={AvatarIconSeverity.Danger} />
<IconAlert severity={IconAlertSeverity.Danger} />
<Tag severity={TagSeverity.Danger}>High risk</Tag>
```

See migration guides for complete instructions:

- [React Migration
Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260)
- [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0280-to-0290)

### ✅ Checklist

- [x] Changelogs updated with human-readable descriptions
- [x] Changelog validation passed (`yarn changelog:validate`)
- [x] Version bumps follow semantic versioning
- [x] design-system-shared: minor (0.21.0 → 0.22.0) - new shared types +
breaking severity rename
- [x] design-system-react: minor (0.25.0 → 0.26.0) - new components +
breaking API changes
- [x] design-system-react-native: minor (0.28.0 → 0.29.0) - new
components + breaking severity rename
- [x] Breaking changes documented with migration guidance
- [x] Migration guides updated with before/after examples
- [x] PR references included in changelog entries

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs)
- [x] I've reviewed the [Release
Workflow](./.cursor/rules/release-workflow.md) cursor rule
- [ ] All tests pass (`yarn build && yarn test && yarn lint`)
- [x] Changelog validation passes (`yarn changelog:validate`)

## **Pre-merge reviewer checklist**

- [ ] I've reviewed the [Reviewing Release
PRs](./docs/reviewing-release-prs.md) guide
- [ ] Package versions follow semantic versioning
- [ ] Changelog entries are consumer-facing (not commit message
regurgitation)
- [ ] Breaking changes are documented in MIGRATION.md with examples
- [ ] All unreleased changes are accounted for in changelogs

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> The release documents multiple breaking consumer APIs (TextButton on
web, severity `.Error`→`.Danger`); upgrading without following
MIGRATION.md will cause compile/runtime mismatches, though this PR does
not change component implementation itself.
> 
> **Overview**
> **Release 44.0.0** cuts new versions of the design-system packages and
records what shipped since the last release: monorepo root **43.0.0 →
44.0.0**, `@metamask/design-system-shared` **0.22.0**,
`@metamask/design-system-react` **0.26.0**, and
`@metamask/design-system-react-native` **0.29.0**.
> 
> Changelogs document **React** additions (`Tag`,
`Toast`/`Toaster`/`toast()`), a **breaking** `TextButton` rewrite
(`size`/`TextButtonSize` → `variant`/`TextVariant`, dropped
inverse/disabled/icons, added `asChild`), and
**`AvatarIconSeverity.Error` → `.Danger`**. **React Native** adds
**`ListItem`** and the same **severity rename** on `AvatarIcon`,
`IconAlert`, and `Tag`. **Shared** adds cross-platform types
(`ListItem`, `Toast`, `TextButton`, `AvatarNetworkSize`) and the shared
**`.Error` → `.Danger`** severity vocabulary.
> 
> **MIGRATION.md** on React and React Native gains **0.25→0.26** and
**0.28→0.29** sections with before/after examples. The only non-release
code change in the diff is reordering the `react-native-worklets`
devDependency in `apps/storybook-react-native/package.json`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c3345f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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.

2 participants