Implement Toast Notification System#64
Conversation
Introduce a global store for managing toast state and a public `toast()` API for triggering notifications from anywhere in the app.
Create the <Toaster /> component which subscribes to the toast store and renders the notifications. Includes: - Support for different toast types (info, success, etc.) - Basic styling and layout - Accessibility props (role="status", aria-live="polite")
Add the <Toaster /> component to the root Layout, making the notification system globally available throughout the application.
Introduce a full test suite for the Toaster component using React Testing Library and Jest. Key areas covered: - Initial render state (renders nothing) - Correct rendering upon `toast()` calls - Automatic dismissal after duration - Pause/resume timer on hover - Manual dismissal via close button - Ensures only one toast is rendered at a time Also includes necessary Jest configuration updates to handle SVG imports and path aliases.
| /** | ||
| * @param {string} message The message to show. | ||
| * @param {ToastOptions} [options] Optional settings for the toast. | ||
| */ | ||
| export const toast = (message, options) => showToast('info', message, options); | ||
|
|
||
| toast.success = (message, options) => showToast('success', message, options); | ||
| toast.error = (message, options) => showToast('error', message, options); | ||
| toast.warning = (message, options) => showToast('warning', message, options); | ||
| toast.info = (message, options) => showToast('info', message, options); |
There was a problem hiding this comment.
- May I know why we need to support "toast" as a function?
- Could we define it in a declarative way?
const toast = { success: () => {}, ... }
// or
export const success = () => {};
export const error = () => {};There was a problem hiding this comment.
In the latest implementation, I've renamed it to showToast which is an object with methods (showToast.success(), showToast.error(), etc.) to avoid naming conflicts with the Toast component.
| * @param {ToastOptions} [options] Optional settings for the toast. | ||
| */ | ||
| const showToast = (type, message, options = {}) => { | ||
| store.update({ |
There was a problem hiding this comment.
Could we handle the default value of ToastState in store instead of this as a user?
There was a problem hiding this comment.
As create-store.js and toast logic have been decoupled, default value handling will be skipped here for the time being.
| /** @type {Set<Function>} */ | ||
| const listeners = new Set(); | ||
|
|
||
| const store = { |
There was a problem hiding this comment.
Refactored store.js to create-store.js, making it constructable through a function. Moved this file to the lib directory.
Change the toast function to an object with methods for each toast type. This improves organization and allows for easier extension in the future. - Updated toast to be an object with methods: info, success, error, warning. - Maintained existing functionality for showing toasts.
Replace the existing toast store with a new generic store implementation that provides subscribe, getState, update, and reset methods. This change enhances the modularity and reusability of the store logic. - Introduced createStore function for generic store creation. - Updated toast-related components to use the new toastStore. - Removed the old store implementation.
Add a validation function to ensure toast state adheres to expected structure and defaults. This includes checks for required fields, valid types, and default values for properties like message, type, duration, and showCloseButton. - Introduced DEFAULT_TOAST_VALUES for default state. - Implemented validateToastState function for state validation. - Updated toastStore's update method to use validated state.
- Update Toaster component to manage multiple toasts using the new structure - Replace the old toast store implementation with a new Toast component
- Update tests to verify rendering of multiple toasts simultaneously - Implement checks for limiting visible toasts to MAX_VISIBLE_TOASTS - Ensure individual toast timers can be paused and resumed on hover - Add functionality for dismissing toasts via close buttons - Refactor existing tests for clarity and improved coverage
- Update showToast to be an object with methods for each toast type - Modify Toaster tests to use the new showToast structure - Ensure existing functionality for displaying toasts is maintained
- Upgrade @testing-library/react to version 14.3.1 in package.json and package-lock.json - Upgrade @testing-library/dom to version 9.3.4 in package-lock.json - Modify import statements in toaster.test.js for improved clarity - Ensure compatibility with updated testing library versions
|
Refactored toast system architecture Key changes:
|
| // After animation duration, remove from queue | ||
| setTimeout(() => { | ||
| onRemove(id); | ||
| }, 300); // Match the transition duration |
There was a problem hiding this comment.
Let's define it as a constant(making it in this file is fine)
| }); | ||
|
|
||
| // Counter to ensure unique IDs | ||
| let idCounter = 0; |
There was a problem hiding this comment.
We could have a generator for this.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
|
|
||
| const handleDismiss = useCallback(() => { | ||
| // Mark as exiting to start fade out animation | ||
| markToastAsExiting(id); |
There was a problem hiding this comment.
Could we also pass markToastAsExiting as a prop instead of importing it from the store module?
- Replace counter-based ID generation with an infinite generator function - Ensure unique IDs are generated consistently for each new toast
- Add PropTypes for toast component to enforce type checking - Introduce onRemoveStart callback to handle toast exit animation - Update Toaster component to pass onRemoveStart to Toast
|
@WendellLiu Thank you for your review! I've implemented all three suggested changes. Please let me know if there's anything else that needs adjustment. |
| // Generate unique ID using counter + timestamp | ||
| const now = Date.now(); | ||
| const uniqueId = now + ++idCounter; | ||
| const uniqueId = idGenerator.next().value; |
There was a problem hiding this comment.
You created the uniqueId by concatenating a timestamp with an increment number. We could still implement it as a generator. What do you think?
There was a problem hiding this comment.
Initially I used Date.now() as ID but added an incrementing counter to avoid collisions. After your generator suggestion, I realized that for toast's short-lived nature, simple incrementing IDs are sufficient and more readable. Since toasts disappear on page reload anyway, cross-session uniqueness isn't needed.
I'd prefer to keep the current approach for simplicity unless you see specific benefits of integrating timestamp into the ID generation?
There was a problem hiding this comment.
I'm comfortable with both options; I just need to confirm your approach. Thank you for your response.
Description
This pull request introduces a toast notification system. The goal is to provide a simple and robust way to display short, non-intrusive messages to the user, such as success confirmations or error alerts.
The implementation is heavily inspired by the clean API and singleton pattern of libraries like sonner, but is built from scratch to ensure full control over behavior, styling, and accessibility.
Implementation Details
store.js: A lightweight, global, singleton store that holds the state of the currently active toast. This ensures that only one toast is ever present on the screen, as new toasts simply overwrite the previous state.toast.js: A public-facing API module that exposes simple functions like toast.success('Message') and toast.error('Message'). Components can call these functions from anywhere in the application without needing to manage state themselves.toaster.js: A React component that subscribes to the store and is responsible for rendering the toast's UI, managing enter/leave animations (via Headless UI), handling timers for auto-dismissal, and pausing on hover. It is integrated into the main Layout.js to be present on all pages.Special attention has been given to accessibility, with the component using role="status" and aria-live="polite" to ensure it is properly announced by screen readers.
How to Test
npm test. All tests for the toaster.test.js suite should pass.Notes
Known Issues (Resolved)