diff --git a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts
index 5600ef333d012497a36470f7a9e073f14e4b4cca..929f2061ebce8d1f8501cd6c075a4a72cafce6dc 100644
--- a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts
+++ b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts
@@ -1,7 +1,7 @@
 import type { DatabaseId, GroupId, Impersonation } from "metabase-types/api";
 import type { PartialBy } from "metabase/core/types";
 
-import type { RequestState } from "metabase-types/store/requests";
+import type { RequestsState, RequestState } from "metabase-types/store";
 import type { EnterpriseState } from "metabase-enterprise/shared/types";
 import type { EnterpriseSharedState } from "metabase-enterprise/shared/reducer";
 import type { AdvancedPermissionsState } from "./reducer";
@@ -9,7 +9,7 @@ import type { AdvancedPermissionsState } from "./reducer";
 export type ImpersonationParams = { groupId: GroupId; databaseId: DatabaseId };
 
 export interface AdvancedPermissionsStoreState extends EnterpriseState {
-  requests: {
+  requests: RequestsState & {
     plugins: {
       advancedPermissionsPlugin: {
         policies: Record<string, RequestState>;
diff --git a/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts b/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts
index a8e331ea8cba575498f5952c2790f90b0bf1d38b..cb232410b2d3dac8195019bd76e9500e3ce8199f 100644
--- a/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts
+++ b/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts
@@ -1,12 +1,12 @@
 import type { EnterpriseSharedState } from "metabase-enterprise/shared/reducer";
 import type { EnterpriseState } from "metabase-enterprise/shared/types";
 import type { GroupTableAccessPolicy } from "metabase-types/api";
-import type { RequestState } from "metabase-types/store/requests";
+import type { RequestsState, RequestState } from "metabase-types/store";
 
 export type GroupTableAccessPolicyParams = { groupId: string; tableId: string };
 
 export interface SandboxesState extends EnterpriseState {
-  requests: {
+  requests: RequestsState & {
     plugins: {
       sandboxesPlugin: {
         policies: Record<string, RequestState>;
diff --git a/frontend/src/metabase-types/store/index.ts b/frontend/src/metabase-types/store/index.ts
index 4bce48841c9ba3857fdaf522a33351405768eb26..da52d6d15894b511c5918a76bba6de198cb04868 100644
--- a/frontend/src/metabase-types/store/index.ts
+++ b/frontend/src/metabase-types/store/index.ts
@@ -6,7 +6,8 @@ export * from "./dashboard";
 export * from "./embed";
 export * from "./entities";
 export * from "./metabot";
-export * from "./settings";
 export * from "./qb";
+export * from "./requests";
+export * from "./settings";
 export * from "./setup";
 export * from "./state";
diff --git a/frontend/src/metabase-types/store/mocks/requests.ts b/frontend/src/metabase-types/store/mocks/requests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..70bba8a01457a088de2f3b42ca366e3acc8c9850
--- /dev/null
+++ b/frontend/src/metabase-types/store/mocks/requests.ts
@@ -0,0 +1,8 @@
+import type { RequestsState } from "../requests";
+
+export const createMockRequestsState = (): RequestsState => {
+  return {
+    entities: {},
+    plugins: {},
+  };
+};
diff --git a/frontend/src/metabase-types/store/mocks/state.ts b/frontend/src/metabase-types/store/mocks/state.ts
index 2e12f7df64eb7fc7b2b5ca8b320249f3fb560cb4..96e324120fc320cb9227cd794c467d34b1161091 100644
--- a/frontend/src/metabase-types/store/mocks/state.ts
+++ b/frontend/src/metabase-types/store/mocks/state.ts
@@ -9,6 +9,7 @@ import { createMockEmbedState } from "./embed";
 import { createMockMetabotState } from "./metabot";
 import { createMockParametersState } from "./parameters";
 import { createMockQueryBuilderState } from "./qb";
+import { createMockRequestsState } from "./requests";
 import { createMockRoutingState } from "./routing";
 import { createMockSettingsState } from "./settings";
 import { createMockSetupState } from "./setup";
@@ -28,6 +29,7 @@ export const createMockState = (
   metabot: createMockMetabotState(),
   parameters: createMockParametersState(),
   qb: createMockQueryBuilderState(),
+  requests: createMockRequestsState(),
   routing: createMockRoutingState(),
   settings: createMockSettingsState(),
   setup: createMockSetupState(),
diff --git a/frontend/src/metabase-types/store/requests.ts b/frontend/src/metabase-types/store/requests.ts
index 5a0770611eddfdcd30912d8c3048eae91cb3e51e..4a85f7c193ab5a36a06e068e61650a56d7a9d37e 100644
--- a/frontend/src/metabase-types/store/requests.ts
+++ b/frontend/src/metabase-types/store/requests.ts
@@ -1,7 +1,24 @@
+type EntityKey = string;
+
+type QueryKey = string;
+
+type RequestType = string; // only "fetch"?
+
+// See initialRequestState in metabase/redux/requests.js
 export interface RequestState {
   loading: boolean;
   loaded: boolean;
   fetched: boolean;
-  error: any | null;
+  error: unknown | null;
   _isRequestState: true;
 }
+
+type RequestsGroupState = Record<
+  EntityKey,
+  Record<QueryKey, Record<RequestType, RequestState>>
+>;
+
+export type RequestsState = {
+  plugins: RequestsGroupState;
+  entities: RequestsGroupState;
+};
diff --git a/frontend/src/metabase-types/store/state.ts b/frontend/src/metabase-types/store/state.ts
index 6cd47a440adf6bc71eb6ceeec0bc4fcc73b39d30..6aebf59cf2c62e1bbb10a3cc035a719761ef5775 100644
--- a/frontend/src/metabase-types/store/state.ts
+++ b/frontend/src/metabase-types/store/state.ts
@@ -9,6 +9,7 @@ import type { EntitiesState } from "./entities";
 import type { MetabotState } from "./metabot";
 import type { QueryBuilderState } from "./qb";
 import type { ParametersState } from "./parameters";
+import type { RequestsState } from "./requests";
 import type { SettingsState } from "./settings";
 import type { SetupState } from "./setup";
 import type { FileUploadState } from "./upload";
@@ -25,6 +26,7 @@ export interface State {
   metabot: MetabotState;
   parameters: ParametersState;
   qb: QueryBuilderState;
+  requests: RequestsState;
   routing: RouterState;
   settings: SettingsState;
   setup: SetupState;
diff --git a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts
index 001968043fa4768dd5ff3b9da20ae1b19fd2bcab..e9a8e3297ef3c3bfa04c970fac1c4377ef190b02 100644
--- a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts
+++ b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts
@@ -164,7 +164,6 @@ function setup({
     dashcards: _.indexBy(dashcards, "id"),
   });
 
-  // @ts-expect-error we need better redux test tooling
   const store = getStore(
     mainReducers,
     createMockState({ dashboard: dashboardState }),
diff --git a/frontend/src/metabase/query_builder/containers/test-utils.tsx b/frontend/src/metabase/query_builder/containers/test-utils.tsx
index 3f73a10d7b85a2a10b351c28d6503cac428db3cd..a177ad67d17dadfcc1e490adc773a1da52d0c8a0 100644
--- a/frontend/src/metabase/query_builder/containers/test-utils.tsx
+++ b/frontend/src/metabase/query_builder/containers/test-utils.tsx
@@ -45,6 +45,7 @@ import {
   SAMPLE_DB_ID,
   createSampleDatabase,
 } from "metabase-types/api/mocks/presets";
+import type { State, RequestState } from "metabase-types/store";
 import NewItemMenu from "metabase/containers/NewItemMenu";
 import { LOAD_COMPLETE_FAVICON } from "metabase/hoc/Favicon";
 import { serializeCardForUrl } from "metabase/lib/card";
@@ -252,7 +253,11 @@ export const setup = async ({
 
   const mockEventListener = jest.spyOn(window, "addEventListener");
 
-  const { container, history } = renderWithProviders(
+  const {
+    store: { getState },
+    container,
+    history,
+  } = renderWithProviders(
     <Route>
       <Route path="/" component={TestHome} />
       <Route path="/model">
@@ -278,7 +283,7 @@ export const setup = async ({
     },
   );
 
-  await waitForLoaderToBeRemoved();
+  await waitForLoadingRequests(getState);
 
   return {
     container,
@@ -287,6 +292,25 @@ export const setup = async ({
   };
 };
 
+const waitForLoadingRequests = async (getState: () => State) => {
+  await waitFor(
+    () => {
+      const requests = getRequests(getState());
+      const areRequestsLoading = requests.some(request => request.loading);
+      expect(areRequestsLoading).toBe(false);
+    },
+    { timeout: 5000 },
+  );
+};
+
+const getRequests = (state: State): RequestState[] => {
+  return Object.values(state.requests).flatMap(group =>
+    Object.values(group).flatMap(entity =>
+      Object.values(entity).flatMap(request => Object.values(request)),
+    ),
+  );
+};
+
 export const startNewNotebookModel = async () => {
   userEvent.click(screen.getByText("Use the notebook editor"));
   await waitForLoaderToBeRemoved();