diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx deleted file mode 100644 index 3aa3f618cad..00000000000 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test } from 'vitest'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { NotificationsProvider } from './NotificationsProvider'; - -describe('NotificationsProvider', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - slots: { - snackbar: {}, - }, - })); - - test('dummy test', () => {}); -}); diff --git a/packages/toolpad-core/src/useNotifications/index.ts b/packages/toolpad-core/src/useNotifications/index.ts index 38ae998b741..79ab6ed6d5c 100644 --- a/packages/toolpad-core/src/useNotifications/index.ts +++ b/packages/toolpad-core/src/useNotifications/index.ts @@ -1,2 +1,2 @@ export * from './useNotifications'; -export * from './NotificationsProvider'; +export * from './notifications'; diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/notifications.tsx similarity index 61% rename from packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx rename to packages/toolpad-core/src/useNotifications/notifications.tsx index 8a7ab8cabb2..29b1eb51522 100644 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ b/packages/toolpad-core/src/useNotifications/notifications.tsx @@ -119,10 +119,12 @@ interface NotificationsState { } interface NotificationsProps { - state: NotificationsState; + subscribe: (cb: () => void) => () => void; + getState: () => NotificationsState; } -function Notifications({ state }: NotificationsProps) { +function Notifications({ subscribe, getState }: NotificationsProps) { + const state = React.useSyncExternalStore(subscribe, getState, getState); const currentNotification = state.queue[0] ?? null; return currentNotification ? ( @@ -148,55 +150,83 @@ const generateId = () => { return id; }; -/** - * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to - * access the notifications API. The notifications are shown in the same order they are requested. - * - * Demos: - * - * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) - * - [useNotifications](https://mui.com/toolpad/core/react-use-notifications/) - * - * API: - * - * - [NotificationsProvider API](https://mui.com/toolpad/core/api/notifications-provider) - */ -function NotificationsProvider(props: NotificationsProviderProps) { - const { children } = props; - const [state, setState] = React.useState({ queue: [] }); - - const show = React.useCallback((message, options = {}) => { +interface NotificationsApi { + show: ShowNotification; + close: CloseNotification; + Provider: React.ComponentType; +} + +export function createNotifications(): NotificationsApi { + let state: NotificationsState = { queue: [] }; + const listeners = new Set<() => void>(); + + const getState = () => state; + const subscribeState = (listener: () => void) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }; + const fireUpdateEvent = () => { + for (const listener of listeners) { + listener(); + } + }; + + const show: ShowNotification = (message, options = {}) => { const notificationKey = options.key ?? `::toolpad-internal::notification::${generateId()}`; - setState((prev) => { - if (prev.queue.some((n) => n.notificationKey === notificationKey)) { - // deduplicate by key - return prev; - } - return { - ...prev, - queue: [...prev.queue, { message, options, notificationKey, open: true }], + if (!state.queue.some((n) => n.notificationKey === notificationKey)) { + state = { + ...state, + queue: [...state.queue, { message, options, notificationKey, open: true }], }; - }); + fireUpdateEvent(); + } + return notificationKey; - }, []); + }; - const close = React.useCallback((key) => { - setState((prev) => ({ - ...prev, - queue: prev.queue.filter((n) => n.notificationKey !== key), - })); - }, []); + const close: CloseNotification = (key) => { + state = { + ...state, + queue: state.queue.filter((n) => n.notificationKey !== key), + }; + fireUpdateEvent(); + }; - const contextValue = React.useMemo(() => ({ show, close }), [show, close]); + const contextValue = { show, close }; - return ( - - - {children} - - - - ); + /** + * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to + * access the notifications API. The notifications are shown in the same order they are requested. + * + * Demos: + * + * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) + * - [useNotifications](https://mui.com/toolpad/core/react-use-notifications/) + * + * API: + * + * - [NotificationsProvider API](https://mui.com/toolpad/core/api/notifications-provider) + */ + function Provider(props: NotificationsProviderProps) { + return ( + + + {props.children} + + + + ); + } + + return { + show, + close, + Provider, + }; } -export { NotificationsProvider }; +const { show, close, Provider } = createNotifications(); + +export { show, close, Provider as NotificationsProvider }; diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx index 9c3d719200d..311e4944f45 100644 --- a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx +++ b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx @@ -7,7 +7,7 @@ import { describe, test, expect } from 'vitest'; import { renderHook, within, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { useNotifications } from './useNotifications'; -import { NotificationsProvider } from './NotificationsProvider'; +import { NotificationsProvider } from './notifications'; interface TestWrapperProps { children: React.ReactNode; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e72272457bc..c0b29908ea5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,7 +201,7 @@ importers: version: 7.37.3(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-e552027-20250112(eslint@8.57.1) + version: 19.0.0-beta-decd7b8-20250118(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 5.1.0 version: 5.1.0(eslint@8.57.1) @@ -6090,8 +6090,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-e552027-20250112: - resolution: {integrity: sha512-VjkIXHouCYyJHgk5HmZ1LH+fAK5CX+ULRX9iNYtwYJ+ljbivFhIT+JJyxNT/USQpCeS2Dt5ahjFeeMv0RRwTww==} + eslint-plugin-react-compiler@19.0.0-beta-decd7b8-20250118: + resolution: {integrity: sha512-qfs+Xo+VcYPbbVLI2tCP+KBGwm0oksAhjFJO1GwOvP+4b18LLcPZu7xopRhUTOaNd+nn1vOp9EQLZC1wMNxSrQ==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -16276,7 +16276,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-e552027-20250112(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-decd7b8-20250118(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2