diff --git a/frontend/src/metabase-types/api/parameters.ts b/frontend/src/metabase-types/api/parameters.ts
index 799f6eeaded1df5f16450155f7dce20dc5f09b58..90c451c0209a5ff1e2de2fa5370da7ca636dfdec 100644
--- a/frontend/src/metabase-types/api/parameters.ts
+++ b/frontend/src/metabase-types/api/parameters.ts
@@ -89,6 +89,11 @@ export type StructuredParameterDimensionTarget = [
 export type ParameterValueOrArray = string | number | Array<any>;
 export type ParameterValue = [RowValue];
 
+export type ParameterValuesMap = Record<
+  ParameterId,
+  ParameterValueOrArray | null
+>;
+
 export interface ParameterValues {
   values: ParameterValue[];
   has_more_values: boolean;
diff --git a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
index 1719af9fda8dcdb738fcb0218967f87773002f9e..92611fc5ae2b46ca0b8484a63b0b106c36bde984 100644
--- a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
+++ b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.tsx
@@ -26,7 +26,7 @@ import type {
   Dashboard,
   Parameter,
   ParameterId,
-  ParameterValueOrArray,
+  ParameterValuesMap,
 } from "metabase-types/api";
 
 import type { DashboardUrlHashOptions } from "../../../dashboard/types";
@@ -48,8 +48,6 @@ import {
 } from "./EmbedFrame.styled";
 import { LogoBadge } from "./LogoBadge";
 
-type ParameterValues = Record<ParameterId, ParameterValueOrArray | null>;
-
 export type EmbedFrameBaseProps = Partial<{
   className: string;
   name: string | null;
@@ -59,8 +57,8 @@ export type EmbedFrameBaseProps = Partial<{
   actionButtons: JSX.Element | null;
   footerVariant: FooterVariant;
   parameters: Parameter[];
-  parameterValues: ParameterValues;
-  draftParameterValues: ParameterValues;
+  parameterValues: ParameterValuesMap;
+  draftParameterValues: ParameterValuesMap;
   hiddenParameterSlugs: string;
   enableParameterRequiredBehavior: boolean;
   setParameterValue: (parameterId: ParameterId, value: any) => void;
@@ -84,7 +82,7 @@ const EMBED_THEME_CLASSES = (theme: DashboardUrlHashOptions["theme"]) => {
   }
 };
 
-function EmbedFrame({
+export const EmbedFrame = ({
   className,
   children,
   name,
@@ -106,15 +104,17 @@ function EmbedFrame({
   theme,
   hide_parameters,
   hide_download_button,
-}: EmbedFrameProps) {
-  const isSdk = useSelector(getIsEmbeddingSdk);
+}: EmbedFrameProps) => {
+  const isEmbeddingSdk = useSelector(getIsEmbeddingSdk);
   const hasEmbedBranding = useSelector(
     state => !getSetting(state, "hide-embed-branding?"),
   );
 
-  const ParametersListComponent = isSdk ? ParametersList : SyncedParametersList;
+  const ParametersListComponent = isEmbeddingSdk
+    ? ParametersList
+    : SyncedParametersList;
 
-  const [hasFrameScroll, setHasFrameScroll] = useState(!isSdk);
+  const [hasFrameScroll, setHasFrameScroll] = useState(!isEmbeddingSdk);
 
   const [hasInnerScroll, setHasInnerScroll] = useState(
     document.documentElement.scrollTop > 0,
@@ -251,7 +251,7 @@ function EmbedFrame({
       )}
     </Root>
   );
-}
+};
 
 function isParametersWidgetContainersSticky(parameterCount: number) {
   if (!isSmallScreen()) {
@@ -262,6 +262,3 @@ function isParametersWidgetContainersSticky(parameterCount: number) {
   // takes too much space on small screens
   return parameterCount <= 5;
 }
-
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export default EmbedFrame;
diff --git a/frontend/src/metabase/public/components/EmbedFrame/SyncedEmbedFrame.tsx b/frontend/src/metabase/public/components/EmbedFrame/SyncedEmbedFrame.tsx
index 9a91bec876b92510c527c395595c006fcb1a0d61..17f77b04ac0e1ed760a6429e423a4d2ab68c4e3d 100644
--- a/frontend/src/metabase/public/components/EmbedFrame/SyncedEmbedFrame.tsx
+++ b/frontend/src/metabase/public/components/EmbedFrame/SyncedEmbedFrame.tsx
@@ -1,33 +1,18 @@
-import { useEffect } from "react";
 import type { WithRouterProps } from "react-router";
 import { withRouter } from "react-router";
 
-import type { DashboardUrlHashOptions } from "metabase/dashboard/types";
-import { parseHashOptions } from "metabase/lib/browser";
-import { isWithinIframe } from "metabase/lib/dom";
-import { useDispatch } from "metabase/lib/redux";
-import { setInitialUrlOptions } from "metabase/redux/embed";
+import { useEmbedFrameOptions } from "metabase/public/hooks";
 
 import type { EmbedFrameProps } from "./EmbedFrame";
-import EmbedFrame from "./EmbedFrame";
+import { EmbedFrame } from "./EmbedFrame";
 
 const SyncedEmbedFrameInner = ({
   location,
   children,
   ...embedFrameProps
 }: EmbedFrameProps & WithRouterProps) => {
-  const dispatch = useDispatch();
-  useEffect(() => {
-    dispatch(setInitialUrlOptions(location));
-  }, [dispatch, location]);
-
-  const {
-    bordered = isWithinIframe(),
-    titled = true,
-    theme,
-    hide_parameters,
-    hide_download_button,
-  } = parseHashOptions(location.hash) as DashboardUrlHashOptions;
+  const { bordered, hide_download_button, hide_parameters, theme, titled } =
+    useEmbedFrameOptions({ location });
 
   return (
     <EmbedFrame
diff --git a/frontend/src/metabase/public/components/EmbedFrame/index.ts b/frontend/src/metabase/public/components/EmbedFrame/index.ts
index 9dcb2a8c450446582f05ad9cb5ae034bc934d658..47f2b5a9e3b3a97537aff10ab672c19a2d600fd0 100644
--- a/frontend/src/metabase/public/components/EmbedFrame/index.ts
+++ b/frontend/src/metabase/public/components/EmbedFrame/index.ts
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export { default } from "./EmbedFrame";
+export { EmbedFrame } from "./EmbedFrame";
 export { SyncedEmbedFrame } from "./SyncedEmbedFrame";
diff --git a/frontend/src/metabase/public/containers/PublicApp/PublicApp.unit.spec.tsx b/frontend/src/metabase/public/containers/PublicApp/PublicApp.unit.spec.tsx
index b4c32449fbeb7d74bc4c77913ac76ca2833411dc..13ca8d2d0ac277cf36b7fc6fd5671d89499d97cf 100644
--- a/frontend/src/metabase/public/containers/PublicApp/PublicApp.unit.spec.tsx
+++ b/frontend/src/metabase/public/containers/PublicApp/PublicApp.unit.spec.tsx
@@ -68,11 +68,7 @@ describe("PublicApp", () => {
 
   it("renders action buttons", () => {
     setup({
-      actionButtons: (
-        <div>
-          <button key="test">Click Me</button>
-        </div>
-      ),
+      actionButtons: <button key="test">Click Me</button>,
     });
     expect(
       screen.getByRole("button", { name: "Click Me" }),
diff --git a/frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard.tsx b/frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard.tsx
index 989ce9da1602f260da1f9f12186a2acc3eadd727..60f78995481b9429126ef954ee776be1c3e95258 100644
--- a/frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard.tsx
+++ b/frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard/PublicOrEmbeddedDashboard.tsx
@@ -48,7 +48,7 @@ import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMo
 import type { Dashboard, DashboardId } from "metabase-types/api";
 import type { State } from "metabase-types/store";
 
-import EmbedFrame from "../../components/EmbedFrame";
+import { EmbedFrame } from "../../components/EmbedFrame";
 
 import { DashboardContainer } from "./PublicOrEmbeddedDashboard.styled";
 
diff --git a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.jsx b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.jsx
deleted file mode 100644
index 98941c6f804d7d249a1603b976e228ec69dbffd4..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.jsx
+++ /dev/null
@@ -1,260 +0,0 @@
-/* eslint-disable react/prop-types */
-
-import cx from "classnames";
-import { updateIn } from "icepick";
-import { Component } from "react";
-import { connect } from "react-redux";
-import _ from "underscore";
-
-import ExplicitSize from "metabase/components/ExplicitSize";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import CS from "metabase/css/core/index.css";
-import title from "metabase/hoc/Title";
-import { getParameterValuesByIdFromQueryParams } from "metabase/parameters/utils/parameter-values";
-import { SyncedEmbedFrame } from "metabase/public/components/EmbedFrame/SyncedEmbedFrame";
-import QueryDownloadWidget from "metabase/query_builder/components/QueryDownloadWidget";
-import { setErrorPage } from "metabase/redux/app";
-import { addParamValues, addFields } from "metabase/redux/metadata";
-import { getMetadata } from "metabase/selectors/metadata";
-import {
-  PublicApi,
-  EmbedApi,
-  setPublicQuestionEndpoints,
-  setEmbedQuestionEndpoints,
-  maybeUsePivotEndpoint,
-} from "metabase/services";
-import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
-import Visualization from "metabase/visualizations/components/Visualization";
-import Question from "metabase-lib/v1/Question";
-import { getCardUiParameters } from "metabase-lib/v1/parameters/utils/cards";
-import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
-import { getParametersFromCard } from "metabase-lib/v1/parameters/utils/template-tags";
-import { applyParameters } from "metabase-lib/v1/queries/utils/card";
-
-const mapStateToProps = state => ({
-  metadata: getMetadata(state),
-});
-
-const mapDispatchToProps = {
-  setErrorPage,
-  addParamValues,
-  addFields,
-};
-
-class PublicOrEmbeddedQuestionInner extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      card: null,
-      result: null,
-      initialized: false,
-      parameterValues: {},
-    };
-  }
-
-  async UNSAFE_componentWillMount() {
-    const {
-      setErrorPage,
-      params: { uuid, token },
-      location: { query },
-    } = this.props;
-
-    if (uuid) {
-      setPublicQuestionEndpoints(uuid);
-    } else if (token) {
-      setEmbedQuestionEndpoints(token);
-    }
-
-    try {
-      let card;
-      if (token) {
-        card = await EmbedApi.card({ token });
-      } else if (uuid) {
-        card = await PublicApi.card({ uuid });
-      } else {
-        throw { status: 404 };
-      }
-
-      if (card.param_values) {
-        await this.props.addParamValues(card.param_values);
-      }
-      if (card.param_fields) {
-        await this.props.addFields(card.param_fields);
-      }
-
-      const parameters = getCardUiParameters(
-        card,
-        this.props.metadata,
-        {},
-        card.parameters || undefined,
-      );
-      const parameterValuesById = getParameterValuesByIdFromQueryParams(
-        parameters,
-        query,
-      );
-
-      this.setState(
-        { card, parameterValues: parameterValuesById },
-        async () => {
-          await this.run();
-          this.setState({ initialized: true });
-        },
-      );
-    } catch (error) {
-      console.error("error", error);
-      setErrorPage(error);
-    }
-  }
-
-  setParameterValue = (parameterId, value) => {
-    this.setState(
-      {
-        parameterValues: {
-          ...this.state.parameterValues,
-          [parameterId]: value,
-        },
-      },
-      this.run,
-    );
-  };
-
-  setParameterValueToDefault = parameterId => {
-    const parameters = this.getParameters();
-    const parameter = parameters.find(({ id }) => id === parameterId);
-    if (parameter) {
-      this.setParameterValue(parameterId, parameter.default);
-    }
-  };
-
-  run = async () => {
-    const {
-      setErrorPage,
-      params: { uuid, token },
-    } = this.props;
-    const { card, parameterValues } = this.state;
-
-    if (!card) {
-      return;
-    }
-
-    const parameters = card.parameters || getParametersFromCard(card);
-
-    try {
-      this.setState({ result: null });
-
-      let newResult;
-      if (token) {
-        // embeds apply parameter values server-side
-        newResult = await maybeUsePivotEndpoint(
-          EmbedApi.cardQuery,
-          card,
-        )({
-          token,
-          ...getParameterValuesBySlug(parameters, parameterValues),
-        });
-      } else if (uuid) {
-        // public links currently apply parameters client-side
-        const datasetQuery = applyParameters(card, parameters, parameterValues);
-        newResult = await maybeUsePivotEndpoint(
-          PublicApi.cardQuery,
-          card,
-        )({
-          uuid,
-          parameters: JSON.stringify(datasetQuery.parameters),
-        });
-      } else {
-        throw { status: 404 };
-      }
-
-      this.setState({ result: newResult });
-    } catch (error) {
-      console.error("error", error);
-      setErrorPage(error);
-    }
-  };
-
-  getParameters() {
-    const { metadata } = this.props;
-    const { card, initialized } = this.state;
-
-    if (!initialized || !card) {
-      return [];
-    }
-
-    return getCardUiParameters(
-      card,
-      metadata,
-      {},
-      card.parameters || undefined,
-    );
-  }
-
-  render() {
-    const {
-      params: { uuid, token },
-      metadata,
-    } = this.props;
-    const { card, result, initialized, parameterValues } = this.state;
-    const question = new Question(card, metadata);
-
-    const actionButtons = result && (
-      <QueryDownloadWidget
-        className={cx(CS.m1, CS.textMediumHover)}
-        question={question}
-        result={result}
-        uuid={uuid}
-        token={token}
-      />
-    );
-
-    return (
-      <SyncedEmbedFrame
-        name={card && card.name}
-        description={card && card.description}
-        actionButtons={actionButtons}
-        question={question}
-        parameters={this.getParameters()}
-        parameterValues={parameterValues}
-        setParameterValue={this.setParameterValue}
-        enableParameterRequiredBehavior
-        setParameterValueToDefault={this.setParameterValueToDefault}
-      >
-        <LoadingAndErrorWrapper
-          className={CS.flexFull}
-          loading={!result || !initialized}
-          error={typeof result === "string" ? result : null}
-          noWrapper
-        >
-          {() => (
-            <Visualization
-              error={result && result.error}
-              rawSeries={[{ card: card, data: result && result.data }]}
-              className={cx(CS.full, CS.flexFull, CS.z1)}
-              onUpdateVisualizationSettings={settings =>
-                this.setState({
-                  card: updateIn(
-                    card,
-                    ["visualization_settings"],
-                    previousSettings => ({ ...previousSettings, ...settings }),
-                  ),
-                })
-              }
-              gridUnit={12}
-              showTitle={false}
-              isDashboard
-              mode={PublicMode}
-              metadata={this.props.metadata}
-              onChangeCardAndRun={() => {}}
-            />
-          )}
-        </LoadingAndErrorWrapper>
-      </SyncedEmbedFrame>
-    );
-  }
-}
-
-export const PublicOrEmbeddedQuestion = _.compose(
-  connect(mapStateToProps, mapDispatchToProps),
-  title(({ card }) => card && card.name),
-  ExplicitSize({ refreshMode: "debounceLeading" }),
-)(PublicOrEmbeddedQuestionInner);
diff --git a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.tsx b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff3bd9b3ae5ce1d1ed6ef22869afdc72035b0199
--- /dev/null
+++ b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.tsx
@@ -0,0 +1,241 @@
+import cx from "classnames";
+import type { Location } from "history";
+import { updateIn } from "icepick";
+import { useCallback, useEffect, useState } from "react";
+import { useMount } from "react-use";
+import _ from "underscore";
+
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import CS from "metabase/css/core/index.css";
+import { useDispatch, useSelector } from "metabase/lib/redux";
+import { getParameterValuesByIdFromQueryParams } from "metabase/parameters/utils/parameter-values";
+import { EmbedFrame } from "metabase/public/components/EmbedFrame";
+import { useEmbedFrameOptions } from "metabase/public/hooks";
+import QueryDownloadWidget from "metabase/query_builder/components/QueryDownloadWidget";
+import { setErrorPage } from "metabase/redux/app";
+import { addParamValues, addFields } from "metabase/redux/metadata";
+import { getMetadata } from "metabase/selectors/metadata";
+import {
+  PublicApi,
+  EmbedApi,
+  setPublicQuestionEndpoints,
+  setEmbedQuestionEndpoints,
+  maybeUsePivotEndpoint,
+} from "metabase/services";
+import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
+import Visualization from "metabase/visualizations/components/Visualization";
+import Question from "metabase-lib/v1/Question";
+import { getCardUiParameters } from "metabase-lib/v1/parameters/utils/cards";
+import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
+import { getParametersFromCard } from "metabase-lib/v1/parameters/utils/template-tags";
+import { applyParameters } from "metabase-lib/v1/queries/utils/card";
+import type {
+  Card,
+  VisualizationSettings,
+  Dataset,
+  ParameterId,
+  ParameterValuesMap,
+} from "metabase-types/api";
+
+export const PublicOrEmbeddedQuestion = ({
+  params: { uuid, token },
+  location,
+}: {
+  location: Location;
+  params: { uuid: string; token: string };
+}) => {
+  const dispatch = useDispatch();
+
+  const metadata = useSelector(getMetadata);
+
+  const [initialized, setInitialized] = useState(false);
+
+  const [card, setCard] = useState<Card | null>(null);
+  const [result, setResult] = useState<Dataset | null>(null);
+  const [parameterValues, setParameterValues] = useState<ParameterValuesMap>(
+    {},
+  );
+
+  useMount(async () => {
+    if (uuid) {
+      setPublicQuestionEndpoints(uuid);
+    } else if (token) {
+      setEmbedQuestionEndpoints(token);
+    }
+
+    try {
+      let card;
+      if (token) {
+        card = await EmbedApi.card({ token });
+      } else if (uuid) {
+        card = await PublicApi.card({ uuid });
+      } else {
+        throw { status: 404 };
+      }
+
+      if (card.param_values) {
+        await dispatch(addParamValues(card.param_values));
+      }
+      if (card.param_fields) {
+        await dispatch(addFields(card.param_fields));
+      }
+
+      const parameters = getCardUiParameters(
+        card,
+        metadata,
+        {},
+        card.parameters || undefined,
+      );
+      const parameterValuesById = getParameterValuesByIdFromQueryParams(
+        parameters,
+        location.query,
+      );
+
+      setCard(card);
+      setParameterValues(parameterValuesById);
+      setInitialized(true);
+    } catch (error) {
+      console.error("error", error);
+      dispatch(setErrorPage(error));
+    }
+  });
+
+  const setParameterValue = async (parameterId: ParameterId, value: any) => {
+    setParameterValues(prevParameterValues => ({
+      ...prevParameterValues,
+      [parameterId]: value,
+    }));
+  };
+
+  const setParameterValueToDefault = (parameterId: ParameterId) => {
+    const parameters = getParameters();
+    const parameter = parameters.find(({ id }) => id === parameterId);
+    if (parameter) {
+      setParameterValue(parameterId, parameter.default);
+    }
+  };
+
+  const run = useCallback(async () => {
+    if (!card) {
+      return;
+    }
+
+    const parameters = card.parameters || getParametersFromCard(card);
+
+    try {
+      setResult(null);
+
+      let newResult;
+      if (token) {
+        // embeds apply parameter values server-side
+        newResult = await maybeUsePivotEndpoint(
+          EmbedApi.cardQuery,
+          card,
+        )({
+          token,
+          ...getParameterValuesBySlug(parameters, parameterValues),
+        });
+      } else if (uuid) {
+        // public links currently apply parameters client-side
+        const datasetQuery = applyParameters(card, parameters, parameterValues);
+        newResult = await maybeUsePivotEndpoint(
+          PublicApi.cardQuery,
+          card,
+        )({
+          uuid,
+          parameters: JSON.stringify(datasetQuery.parameters),
+        });
+      } else {
+        throw { status: 404 };
+      }
+
+      setResult(newResult);
+    } catch (error) {
+      console.error("error", error);
+      dispatch(setErrorPage(error));
+    }
+  }, [card, dispatch, parameterValues, token, uuid]);
+
+  useEffect(() => {
+    run();
+  }, [run]);
+
+  const getParameters = () => {
+    if (!initialized || !card) {
+      return [];
+    }
+
+    return getCardUiParameters(
+      card,
+      metadata,
+      {},
+      card.parameters || undefined,
+    );
+  };
+
+  const question = new Question(card, metadata);
+
+  const actionButtons = result && (
+    <QueryDownloadWidget
+      className={cx(CS.m1, CS.textMediumHover)}
+      question={question}
+      result={result}
+      uuid={uuid}
+      token={token}
+    />
+  );
+
+  const { bordered, hide_download_button, hide_parameters, theme, titled } =
+    useEmbedFrameOptions({ location });
+
+  return (
+    <EmbedFrame
+      name={card && card.name}
+      description={card && card.description}
+      actionButtons={actionButtons}
+      question={question}
+      parameters={getParameters()}
+      parameterValues={parameterValues}
+      setParameterValue={setParameterValue}
+      enableParameterRequiredBehavior
+      setParameterValueToDefault={setParameterValueToDefault}
+      bordered={bordered}
+      hide_download_button={hide_download_button}
+      hide_parameters={hide_parameters}
+      theme={theme}
+      titled={titled}
+    >
+      <LoadingAndErrorWrapper
+        className={CS.flexFull}
+        loading={!result}
+        error={typeof result === "string" ? result : null}
+        noWrapper
+      >
+        {() => (
+          <Visualization
+            error={result && result.error}
+            rawSeries={[{ card: card, data: result && result.data }]}
+            className={cx(CS.full, CS.flexFull, CS.z1)}
+            onUpdateVisualizationSettings={(
+              settings: VisualizationSettings,
+            ) => {
+              setCard(prevCard =>
+                updateIn(
+                  prevCard,
+                  ["visualization_settings"],
+                  previousSettings => ({ ...previousSettings, ...settings }),
+                ),
+              );
+            }}
+            gridUnit={12}
+            showTitle={false}
+            isDashboard
+            mode={PublicMode}
+            metadata={metadata}
+            onChangeCardAndRun={() => {}}
+          />
+        )}
+      </LoadingAndErrorWrapper>
+    </EmbedFrame>
+  );
+};
diff --git a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.unit.spec.tsx b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.unit.spec.tsx
index 97da0f6092e0afea02334e9553deeb0e865550dd..29af2501867b1d4703c8847a32a18c33882c61ec 100644
--- a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.unit.spec.tsx
+++ b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestion.unit.spec.tsx
@@ -5,7 +5,11 @@ import {
   setupPublicCardQueryEndpoints,
   setupPublicQuestionEndpoints,
 } from "__support__/server-mocks";
-import { renderWithProviders, screen } from "__support__/ui";
+import {
+  renderWithProviders,
+  screen,
+  waitForLoaderToBeRemoved,
+} from "__support__/ui";
 import registerVisualizations from "metabase/visualizations/register";
 import type { VisualizationProps } from "metabase/visualizations/types";
 import {
@@ -87,12 +91,16 @@ describe("PublicOrEmbeddedQuestion", () => {
   it("should update card settings when visualization component changes them (metabase#37429)", async () => {
     await setup();
 
+    await waitForLoaderToBeRemoved();
+
     await userEvent.click(
       await screen.findByRole("button", {
         name: /update settings/i,
       }),
     );
 
+    await waitForLoaderToBeRemoved();
+
     expect(screen.getByTestId("settings")).toHaveTextContent(
       JSON.stringify({ foo: "bar" }),
     );
diff --git a/frontend/src/metabase/public/hooks/index.ts b/frontend/src/metabase/public/hooks/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08f527bd75ab80a418fb9a8a68cac75584392b54
--- /dev/null
+++ b/frontend/src/metabase/public/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./use-embed-frame-options";
diff --git a/frontend/src/metabase/public/hooks/use-embed-frame-options.ts b/frontend/src/metabase/public/hooks/use-embed-frame-options.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9ef8a82ea8909b7ce44cf5fde420e0aa59bf7ef8
--- /dev/null
+++ b/frontend/src/metabase/public/hooks/use-embed-frame-options.ts
@@ -0,0 +1,31 @@
+import type { Location } from "history";
+import { useEffect } from "react";
+
+import type { DashboardUrlHashOptions } from "metabase/dashboard/types";
+import { parseHashOptions } from "metabase/lib/browser";
+import { isWithinIframe } from "metabase/lib/dom";
+import { useDispatch } from "metabase/lib/redux";
+import { setInitialUrlOptions } from "metabase/redux/embed";
+
+export const useEmbedFrameOptions = ({ location }: { location: Location }) => {
+  const dispatch = useDispatch();
+  useEffect(() => {
+    dispatch(setInitialUrlOptions(location));
+  }, [dispatch, location]);
+
+  const {
+    bordered = isWithinIframe(),
+    titled = true,
+    theme,
+    hide_parameters,
+    hide_download_button,
+  } = parseHashOptions(location.hash) as DashboardUrlHashOptions;
+
+  return {
+    bordered,
+    titled,
+    theme,
+    hide_parameters,
+    hide_download_button,
+  };
+};