fix(react): support pre-mount toast calls#1217
Conversation
📖 Storybook Preview |
| } | ||
| return registeredRef.current; | ||
| let storeToastOptions: ToastOptions | undefined; | ||
| const toastStoreListeners = new Set<ToastStoreListener>(); |
There was a problem hiding this comment.
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, | ||
| () => ({ |
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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)
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
📖 Storybook Preview |
## 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 -->
Description
This PR updates the React Toast imperative API so
toast(...)andtoast.dismiss()work before<Toaster />mounts.The previous implementation registered the mounted
Toasterref 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:
onClose={onClose}directly.toast(...)andtoast.dismiss().Related issues
Fixes:
Manual testing steps
cd packages/design-system-react.yarn test --runInBand --runTestsByPath src/components/Toast/Toast.test.tsx src/components/Toast/Toaster.test.tsx --coverage=false.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
Pre-merge reviewer checklist
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 (viauseLayoutEffect), 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 />passesonClosethrough toBannerBasedirectly 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.