diff --git a/frontend/src/metabase-types/store/app.ts b/frontend/src/metabase-types/store/app.ts
index ca24d79665f8e891570014a75038ee5c2e045c09..0e267770fc7c891546656342aeba106855610705 100644
--- a/frontend/src/metabase-types/store/app.ts
+++ b/frontend/src/metabase-types/store/app.ts
@@ -14,9 +14,23 @@ export interface AppBreadCrumbs {
   show: boolean;
 }
 
+/**
+ * Storage for non-critical, ephemeral user preferences.
+ * Think of it as a sessionStorage alternative implemented in Redux.
+ * Only specific key/value pairs can be stored here,
+ * and then later used with the `use-temp-storage` hook.
+ */
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type TempStorage = {};
+
+export type TempStorageKey = keyof TempStorage;
+export type TempStorageValue<Key extends TempStorageKey = TempStorageKey> =
+  TempStorage[Key];
+
 export interface AppState {
   errorPage: AppErrorDescriptor | null;
   isNavbarOpen: boolean;
   isDndAvailable: boolean;
   isErrorDiagnosticsOpen: boolean;
+  tempStorage: TempStorage;
 }
diff --git a/frontend/src/metabase-types/store/mocks/app.ts b/frontend/src/metabase-types/store/mocks/app.ts
index adf4cb6ed34075304507758f8c73e396295c591e..6bc1f50f756ee72310f7fece48559a8b49d63d1c 100644
--- a/frontend/src/metabase-types/store/mocks/app.ts
+++ b/frontend/src/metabase-types/store/mocks/app.ts
@@ -5,5 +5,6 @@ export const createMockAppState = (opts?: Partial<AppState>): AppState => ({
   errorPage: null,
   isDndAvailable: false,
   isErrorDiagnosticsOpen: false,
+  tempStorage: {},
   ...opts,
 });
diff --git a/frontend/src/metabase/common/hooks/use-temp-storage/index.ts b/frontend/src/metabase/common/hooks/use-temp-storage/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7958d81a2512b81192725a477124dcc1f01f9561
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-temp-storage/index.ts
@@ -0,0 +1 @@
+export * from "./use-temp-storage";
diff --git a/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.ts b/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cda0045e876278401533a569a2a5236d91de48f1
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.ts
@@ -0,0 +1,26 @@
+import { useCallback } from "react";
+
+import { useDispatch, useSelector } from "metabase/lib/redux";
+import { setTempSetting } from "metabase/redux/app";
+import type {
+  State,
+  TempStorageKey,
+  TempStorageValue,
+} from "metabase-types/store";
+
+export const useTempStorage = <Key extends TempStorageKey>(
+  key: Key,
+): [TempStorageValue<Key>, (newValue: TempStorageValue<Key>) => void] => {
+  const dispatch = useDispatch();
+
+  const value = useSelector((state: State) => state.app.tempStorage[key]);
+
+  const setValue = useCallback(
+    (newValue: TempStorageValue<Key>) => {
+      dispatch(setTempSetting({ key, value: newValue }));
+    },
+    [dispatch, key],
+  );
+
+  return [value, setValue];
+};
diff --git a/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..83105bb8636c1de4e9a146fa8e08766fb69cf949
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-temp-storage/use-temp-storage.unit.spec.tsx
@@ -0,0 +1,76 @@
+import userEvent from "@testing-library/user-event";
+
+import { renderWithProviders, screen } from "__support__/ui";
+import type {
+  TempStorage,
+  TempStorageKey,
+  TempStorageValue,
+} from "metabase-types/store";
+import {
+  createMockAppState,
+  createMockState,
+} from "metabase-types/store/mocks";
+
+import { useTempStorage } from "./use-temp-storage";
+
+const TestComponent = ({
+  entry,
+  newValue,
+}: {
+  entry: TempStorageKey;
+  newValue?: TempStorageValue;
+}) => {
+  const [value, setValue] = useTempStorage(entry);
+
+  return (
+    <div>
+      {/* @ts-expect-error - The hook still doesn't accept any k/v pair */}
+      <button onClick={() => setValue(newValue)} />
+      <div data-testid="result">{`Value is: ${value}`}</div>
+    </div>
+  );
+};
+
+type SetupProps = {
+  tempStorage: TempStorage;
+  entry: TempStorageKey;
+  newValue?: TempStorageValue;
+};
+
+const setup = ({ tempStorage = {}, entry, newValue }: SetupProps) => {
+  const initialState = createMockState({
+    app: createMockAppState({ tempStorage }),
+  });
+
+  renderWithProviders(<TestComponent entry={entry} newValue={newValue} />, {
+    storeInitialState: initialState,
+  });
+};
+
+describe("useTempStorage hook", () => {
+  it("should return undefined for uninitialized key", () => {
+    const tempStorage = {
+      animal: undefined,
+    };
+    // @ts-expect-error - The hook still doesn't accept any k/v pair
+    setup({ tempStorage, entry: "animal" });
+
+    expect(screen.getByTestId("result")).toHaveTextContent(
+      "Value is: undefined",
+    );
+  });
+
+  it("should read and set the value", async () => {
+    const tempStorage = {
+      animal: "dog",
+    };
+
+    // @ts-expect-error - The hook still doesn't accept any k/v pair
+    setup({ tempStorage, entry: "animal", newValue: "cat" });
+
+    expect(screen.getByTestId("result")).toHaveTextContent("Value is: dog");
+
+    await userEvent.click(screen.getByRole("button"));
+    expect(screen.getByTestId("result")).toHaveTextContent("Value is: cat");
+  });
+});
diff --git a/frontend/src/metabase/redux/app.ts b/frontend/src/metabase/redux/app.ts
index 420bd3cd5ad6dec758ce2f97a1591b7a2d115ede..135c48d74ee6f2e7c7ec0f6fd52301787dc8f94b 100644
--- a/frontend/src/metabase/redux/app.ts
+++ b/frontend/src/metabase/redux/app.ts
@@ -1,4 +1,8 @@
-import { createAction } from "@reduxjs/toolkit";
+import {
+  type PayloadAction,
+  createAction,
+  createSlice,
+} from "@reduxjs/toolkit";
 import { LOCATION_CHANGE, push } from "react-router-redux";
 
 import {
@@ -7,7 +11,12 @@ import {
   shouldOpenInBlankWindow,
 } from "metabase/lib/dom";
 import { combineReducers, handleActions } from "metabase/lib/redux";
-import type { Dispatch } from "metabase-types/store";
+import type {
+  Dispatch,
+  TempStorage,
+  TempStorageKey,
+  TempStorageValue,
+} from "metabase-types/store";
 
 interface LocationChangeAction {
   type: string; // "@@router/LOCATION_CHANGE"
@@ -107,6 +116,24 @@ const isErrorDiagnosticsOpen = handleActions(
   false,
 );
 
+const tempStorageSlice = createSlice({
+  name: "tempStorage",
+  initialState: {} as TempStorage,
+  reducers: {
+    setTempSetting: (
+      state,
+      action: PayloadAction<{
+        key: TempStorageKey;
+        value: TempStorageValue<TempStorageKey>;
+      }>,
+    ) => {
+      state[action.payload.key] = action.payload.value;
+    },
+  },
+});
+
+export const { setTempSetting } = tempStorageSlice.actions;
+
 // eslint-disable-next-line import/no-default-export -- deprecated usage
 export default combineReducers({
   errorPage,
@@ -118,4 +145,5 @@ export default combineReducers({
     return true;
   },
   isErrorDiagnosticsOpen,
+  tempStorage: tempStorageSlice.reducer,
 });