diff --git a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx
index d2841532942..aa69fc4ed52 100644
--- a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx
+++ b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx
@@ -22,6 +22,10 @@ interface ModalsProviderProps {
   props?: any;
 }
 
+/**
+ * @deprecated.
+ * Not the real implementation used by core.
+ */
 export class ModalsProvider extends Component<ModalsProviderProps, ModalsContextState> {
   constructor(props: ModalsProviderProps) {
     super(props);
diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx
index 9cf0b4d0aaf..6a527baa769 100644
--- a/public/app/AppWrapper.tsx
+++ b/public/app/AppWrapper.tsx
@@ -5,7 +5,7 @@ import { Router, Redirect, Switch, RouteComponentProps } from 'react-router-dom'
 import { CompatRouter, CompatRoute } from 'react-router-dom-v5-compat';
 
 import { config, locationService, navigationLogger, reportInteraction } from '@grafana/runtime';
-import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider, PortalContainer } from '@grafana/ui';
+import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, PortalContainer } from '@grafana/ui';
 import { getAppRoutes } from 'app/routes/routes';
 import { store } from 'app/store/store';
 
@@ -15,6 +15,7 @@ import { GrafanaApp } from './app';
 import { AppChrome } from './core/components/AppChrome/AppChrome';
 import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
 import { GrafanaContext } from './core/context/GrafanaContext';
+import { ModalsContextProvider } from './core/context/ModalsContextProvider';
 import { GrafanaRoute } from './core/navigation/GrafanaRoute';
 import { RouteDescriptor } from './core/navigation/types';
 import { contextSrv } from './core/services/context_srv';
@@ -102,11 +103,11 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
                 actions={[]}
                 options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }}
               >
-                <ModalsProvider>
-                  <GlobalStyles />
-                  <div className="grafana-app">
-                    <Router history={locationService.getHistory()}>
-                      <CompatRouter>
+                <Router history={locationService.getHistory()}>
+                  <CompatRouter>
+                    <ModalsContextProvider>
+                      <GlobalStyles />
+                      <div className="grafana-app">
                         <AppChrome>
                           {pageBanners.map((Banner, index) => (
                             <Banner key={index.toString()} />
@@ -118,13 +119,13 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
                             <Hook key={index.toString()} />
                           ))}
                         </AppChrome>
-                      </CompatRouter>
-                    </Router>
-                  </div>
-                  <LiveConnectionWarning />
-                  <ModalRoot />
-                  <PortalContainer />
-                </ModalsProvider>
+                      </div>
+                      <LiveConnectionWarning />
+                      <ModalRoot />
+                      <PortalContainer />
+                    </ModalsContextProvider>
+                  </CompatRouter>
+                </Router>
               </KBarProvider>
             </ThemeProvider>
           </GrafanaContext.Provider>
diff --git a/public/app/app.ts b/public/app/app.ts
index 613b40d560c..5796b18c21e 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -58,7 +58,6 @@ import { initIconCache } from './core/icons/iconBundle';
 import { initializeI18n } from './core/internationalization';
 import { setMonacoEnv } from './core/monacoEnv';
 import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
-import { ModalManager } from './core/services/ModalManager';
 import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker';
 import { backendSrv } from './core/services/backend_srv';
 import { contextSrv } from './core/services/context_srv';
@@ -207,10 +206,6 @@ export class GrafanaApp {
       setDataSourceSrv(dataSourceSrv);
       initWindowRuntime();
 
-      // init modal manager
-      const modalManager = new ModalManager();
-      modalManager.init();
-
       let preloadResults: PluginPreloadResult[] = [];
 
       if (contextSrv.user.orgRole !== '') {
diff --git a/public/app/core/components/modals/AngularModalProxy.tsx b/public/app/core/components/modals/AngularModalProxy.tsx
deleted file mode 100644
index fe21787dc3b..00000000000
--- a/public/app/core/components/modals/AngularModalProxy.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-
-import { ModalRoot, ModalsProvider } from '@grafana/ui';
-
-import { connectWithProvider } from '../../utils/connectWithReduxStore';
-
-/**
- * Component that enables rendering React modals from Angular
- */
-export const AngularModalProxy = connectWithProvider((props: Record<string, unknown>) => {
-  return (
-    <>
-      <ModalsProvider {...props}>
-        <ModalRoot />
-      </ModalsProvider>
-    </>
-  );
-});
diff --git a/public/app/core/context/ModalsContextProvider.tsx b/public/app/core/context/ModalsContextProvider.tsx
new file mode 100644
index 00000000000..897588a7139
--- /dev/null
+++ b/public/app/core/context/ModalsContextProvider.tsx
@@ -0,0 +1,124 @@
+import React, { useEffect, useMemo, useState } from 'react';
+
+import { textUtil } from '@grafana/data';
+import { locationService } from '@grafana/runtime';
+import { ConfirmModal, ConfirmModalProps, ModalsContext } from '@grafana/ui';
+import { ModalsContextState } from '@grafana/ui/src/components/Modal/ModalsContext';
+import { ShowConfirmModalEvent, ShowModalReactEvent } from 'app/types/events';
+
+import appEvents from '../app_events';
+
+export interface Props {
+  children: React.ReactNode;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+interface StateType<TProps = any> {
+  component: React.ComponentType<TProps> | null;
+  props: TProps;
+}
+
+/**
+ * Implements the ModalsContext state logic (not used that much, only needed in edge cases)
+ * Also implements the handling of the events ShowModalReactEvent and ShowConfirmModalEvent.
+ */
+export function ModalsContextProvider(props: Props) {
+  const [state, setState] = useState<StateType>({
+    component: null,
+    props: {},
+  });
+
+  const contextValue: ModalsContextState = useMemo(() => {
+    function showModal<TProps = {}>(component: React.ComponentType<TProps>, props: TProps) {
+      setState({ component, props });
+    }
+
+    function hideModal() {
+      setState({ component: null, props: {} });
+    }
+
+    return {
+      component: state.component,
+      props: {
+        ...state.props,
+        isOpen: true,
+        onDismiss: hideModal,
+      },
+      showModal,
+      hideModal,
+    };
+  }, [state]);
+
+  useEffect(() => {
+    appEvents.subscribe(ShowModalReactEvent, ({ payload }) => {
+      setState({
+        component: payload.component,
+        props: payload.props,
+      });
+    });
+
+    appEvents.subscribe(ShowConfirmModalEvent, (e) => {
+      showConfirmModal(e, setState);
+    });
+
+    // Dismiss the modal when the route changes (if there's a link in the modal)
+    let prevPath = '';
+    locationService.getHistory().listen((location) => {
+      if (location.pathname !== prevPath) {
+        setState({ component: null, props: {} });
+      }
+      prevPath = location.pathname;
+    });
+  }, []);
+
+  return <ModalsContext.Provider value={contextValue}>{props.children}</ModalsContext.Provider>;
+}
+
+function showConfirmModal({ payload }: ShowConfirmModalEvent, setState: (state: StateType) => void) {
+  const {
+    confirmText,
+    onConfirm = () => undefined,
+    onDismiss,
+    text2,
+    altActionText,
+    onAltAction,
+    noText,
+    text,
+    text2htmlBind,
+    yesText = 'Yes',
+    icon,
+    title = 'Confirm',
+    yesButtonVariant,
+  } = payload;
+
+  const hideModal = () => setState({ component: null, props: {} });
+
+  const props: ConfirmModalProps = {
+    confirmText: yesText,
+    confirmButtonVariant: yesButtonVariant,
+    confirmationText: confirmText,
+    icon,
+    title,
+    body: text,
+    description: text2 && text2htmlBind ? textUtil.sanitize(text2) : text2,
+    isOpen: true,
+    dismissText: noText,
+    onConfirm: () => {
+      onConfirm();
+      hideModal();
+    },
+    onDismiss: () => {
+      onDismiss?.();
+      hideModal();
+    },
+    onAlternative: onAltAction
+      ? () => {
+          onAltAction();
+          hideModal();
+        }
+      : undefined,
+    alternativeText: altActionText,
+  };
+
+  setState({ component: ConfirmModal, props });
+}
diff --git a/public/app/core/services/ModalManager.ts b/public/app/core/services/ModalManager.ts
deleted file mode 100644
index 1c04169bcda..00000000000
--- a/public/app/core/services/ModalManager.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from 'react';
-import { createRoot } from 'react-dom/client';
-
-import { textUtil } from '@grafana/data';
-import { config, CopyPanelEvent } from '@grafana/runtime';
-import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
-import appEvents from 'app/core/app_events';
-import { copyPanel } from 'app/features/dashboard/utils/panel';
-
-import {
-  ShowConfirmModalEvent,
-  ShowConfirmModalPayload,
-  ShowModalReactEvent,
-  ShowModalReactPayload,
-} from '../../types/events';
-import { AngularModalProxy } from '../components/modals/AngularModalProxy';
-import { provideTheme } from '../utils/ConfigProvider';
-
-export class ModalManager {
-  reactModalRoot = document.body;
-  reactModalNode = document.createElement('div');
-  root = createRoot(this.reactModalNode);
-
-  init() {
-    appEvents.subscribe(ShowConfirmModalEvent, (e) => this.showConfirmModal(e.payload));
-    appEvents.subscribe(ShowModalReactEvent, (e) => this.showModalReact(e.payload));
-    appEvents.subscribe(CopyPanelEvent, (e) => copyPanel(e.payload));
-  }
-
-  showModalReact(options: ShowModalReactPayload) {
-    const { component, props } = options;
-    const modalProps = {
-      component,
-      props: {
-        ...props,
-        isOpen: true,
-        onDismiss: this.onReactModalDismiss,
-      },
-    };
-
-    const elem = React.createElement(provideTheme(AngularModalProxy, config.theme2), modalProps);
-    this.reactModalRoot.appendChild(this.reactModalNode);
-    this.root.render(elem);
-  }
-
-  onReactModalDismiss = () => {
-    this.root.render(null);
-    this.reactModalRoot.removeChild(this.reactModalNode);
-  };
-
-  showConfirmModal(payload: ShowConfirmModalPayload) {
-    const {
-      confirmText,
-      onConfirm = () => undefined,
-      onDismiss,
-      text2,
-      altActionText,
-      onAltAction,
-      noText,
-      text,
-      text2htmlBind,
-      yesText = 'Yes',
-      icon,
-      title = 'Confirm',
-      yesButtonVariant,
-    } = payload;
-    const props: ConfirmModalProps = {
-      confirmText: yesText,
-      confirmButtonVariant: yesButtonVariant,
-      confirmationText: confirmText,
-      icon,
-      title,
-      body: text,
-      description: text2 && text2htmlBind ? textUtil.sanitize(text2) : text2,
-      isOpen: true,
-      dismissText: noText,
-      onConfirm: () => {
-        onConfirm();
-        this.onReactModalDismiss();
-      },
-      onDismiss: () => {
-        onDismiss?.();
-        this.onReactModalDismiss();
-      },
-      onAlternative: onAltAction
-        ? () => {
-            onAltAction();
-            this.onReactModalDismiss();
-          }
-        : undefined,
-      alternativeText: altActionText,
-    };
-    const modalProps = {
-      component: ConfirmModal,
-      props,
-    };
-
-    const elem = React.createElement(provideTheme(AngularModalProxy, config.theme2), modalProps);
-    this.reactModalRoot.appendChild(this.reactModalNode);
-    this.root.render(elem);
-  }
-}
diff --git a/public/app/features/alerting/unified/integration/AlertRulesDrawer.tsx b/public/app/features/alerting/unified/integration/AlertRulesDrawer.tsx
index 4c6e6431ca2..d22ac005efd 100644
--- a/public/app/features/alerting/unified/integration/AlertRulesDrawer.tsx
+++ b/public/app/features/alerting/unified/integration/AlertRulesDrawer.tsx
@@ -11,12 +11,12 @@ const AlertRulesDrawerContent = React.lazy(
 
 interface Props {
   dashboardUid: string;
-  onClose: () => void;
+  onDismiss: () => void;
 }
 
-export function AlertRulesDrawer({ dashboardUid, onClose }: Props) {
+export function AlertRulesDrawer({ dashboardUid, onDismiss }: Props) {
   return (
-    <Drawer title="Alert rules" subtitle={<DrawerSubtitle dashboardUid={dashboardUid} />} onClose={onClose} size="lg">
+    <Drawer title="Alert rules" subtitle={<DrawerSubtitle dashboardUid={dashboardUid} />} onClose={onDismiss} size="lg">
       <React.Suspense fallback={<LoadingPlaceholder text="Loading alert rules" />}>
         <AlertRulesDrawerContent dashboardUid={dashboardUid} />
       </React.Suspense>
diff --git a/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx b/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx
index 963ceec182d..d7f4b18b857 100644
--- a/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx
+++ b/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
+import React, { useContext } from 'react';
 
-import { ToolbarButton } from '@grafana/ui';
+import { ModalsContext, ToolbarButton } from '@grafana/ui';
 
 import { t } from '../../../../core/internationalization';
-import { useDashNavModalController } from '../../../dashboard/components/DashNav/DashNav';
 import { alertRuleApi } from '../api/alertRuleApi';
 import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
 
@@ -14,7 +13,7 @@ interface AlertRulesToolbarButtonProps {
 }
 
 export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesToolbarButtonProps) {
-  const { showModal, hideModal } = useDashNavModalController();
+  const { showModal, hideModal } = useContext(ModalsContext);
 
   const { data: namespaces = [] } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({
     ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
@@ -25,11 +24,18 @@ export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesTool
     return null;
   }
 
+  const onShowDrawer = () => {
+    showModal(AlertRulesDrawer, {
+      dashboardUid: dashboardUid,
+      onDismiss: hideModal,
+    });
+  };
+
   return (
     <ToolbarButton
       tooltip={t('dashboard.toolbar.alert-rules', 'Alert rules')}
       icon="bell"
-      onClick={() => showModal(<AlertRulesDrawer dashboardUid={dashboardUid} onClose={hideModal} />)}
+      onClick={onShowDrawer}
       key="button-alerting"
     />
   );
diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
index 7950a1f1042..4be544ba1ec 100644
--- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
+++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
@@ -9,7 +9,6 @@ import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
 import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
 import { contextSrv } from 'app/core/core';
 import { Trans, t } from 'app/core/internationalization';
-import { DashNavModalContextProvider, DashNavModalRoot } from 'app/features/dashboard/components/DashNav/DashNav';
 import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
 import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
 
@@ -510,12 +509,7 @@ export function ToolbarActions({ dashboard }: Props) {
     lastGroup = action.group;
   }
 
-  return (
-    <DashNavModalContextProvider>
-      <ToolbarButtonRow alignment="right">{actionElements}</ToolbarButtonRow>
-      <DashNavModalRoot />
-    </DashNavModalContextProvider>
-  );
+  return <ToolbarButtonRow alignment="right">{actionElements}</ToolbarButtonRow>;
 }
 
 function addDynamicActions(
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx
index c9a88c6e97f..181677d5f2b 100644
--- a/public/app/features/dashboard/components/DashNav/DashNav.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx
@@ -2,7 +2,6 @@ import { css } from '@emotion/css';
 import React, { ReactNode } from 'react';
 import { connect, ConnectedProps } from 'react-redux';
 import { useLocation } from 'react-router-dom';
-import { createStateContext } from 'react-use';
 
 import { textUtil } from '@grafana/data';
 import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
@@ -49,27 +48,6 @@ const mapDispatchToProps = {
   updateTimeZoneForSession,
 };
 
-export const [useDashNavModelContext, DashNavModalContextProvider] = createStateContext<{ component: React.ReactNode }>(
-  {
-    component: null,
-  }
-);
-
-export function useDashNavModalController() {
-  const [_, setContextState] = useDashNavModelContext();
-
-  return {
-    showModal: (component: React.ReactNode) => setContextState({ component }),
-    hideModal: () => setContextState({ component: null }),
-  };
-}
-
-export function DashNavModalRoot() {
-  const [contextState] = useDashNavModelContext();
-
-  return <>{contextState.component}</>;
-}
-
 const connector = connect(null, mapDispatchToProps);
 
 const selectors = e2eSelectors.pages.Dashboard.DashNav;
@@ -363,12 +341,11 @@ export const DashNav = React.memo<Props>((props) => {
   return (
     <AppChromeUpdate
       actions={
-        <DashNavModalContextProvider>
+        <>
           {renderLeftActions()}
           <NavToolbarSeparator leftActionsSeparator />
           <ToolbarButtonRow alignment="right">{renderRightActions()}</ToolbarButtonRow>
-          <DashNavModalRoot />
-        </DashNavModalContextProvider>
+        </>
       }
     />
   );
diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx
index d36844ac808..ae104700620 100644
--- a/public/app/features/org/OrgDetailsPage.test.tsx
+++ b/public/app/features/org/OrgDetailsPage.test.tsx
@@ -5,7 +5,6 @@ import { mockToolkitActionCreator } from 'test/core/redux/mocks';
 import { TestProvider } from 'test/helpers/TestProvider';
 
 import { NavModel } from '@grafana/data';
-import { ModalManager } from 'app/core/services/ModalManager';
 
 import { backendSrv } from '../../core/services/backend_srv';
 import { Organization } from '../../types';
@@ -81,7 +80,6 @@ describe('Render', () => {
   });
 
   it('should show a modal when submitting', async () => {
-    new ModalManager().init();
     setup({
       organization: {
         name: 'Cool org',
diff --git a/public/test/helpers/TestProvider.tsx b/public/test/helpers/TestProvider.tsx
index b7b232f041a..873c85c415b 100644
--- a/public/test/helpers/TestProvider.tsx
+++ b/public/test/helpers/TestProvider.tsx
@@ -5,7 +5,9 @@ import { Router } from 'react-router-dom';
 import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
 
 import { locationService } from '@grafana/runtime';
+import { ModalRoot } from '@grafana/ui';
 import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext';
+import { ModalsContextProvider } from 'app/core/context/ModalsContextProvider';
 import { configureStore } from 'app/store/configureStore';
 import { StoreState } from 'app/types/store';
 
@@ -30,7 +32,10 @@ export function TestProvider(props: Props) {
   return (
     <Provider store={store}>
       <Router history={locationService.getHistory()}>
-        <GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
+        <ModalsContextProvider>
+          <GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
+          <ModalRoot />
+        </ModalsContextProvider>
       </Router>
     </Provider>
   );