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