diff --git a/frontend/src/metabase-lib/parameters/utils/click-behavior.js b/frontend/src/metabase-lib/parameters/utils/click-behavior.js
index f94ef827255f27df4fd50617291d01d81f034752..b29f22b93915178a0263c4e9444183bc3082ae75 100644
--- a/frontend/src/metabase-lib/parameters/utils/click-behavior.js
+++ b/frontend/src/metabase-lib/parameters/utils/click-behavior.js
@@ -9,7 +9,6 @@ import {
 } from "metabase-lib/parameters/utils/filters";
 import { isa, isDate } from "metabase-lib/types/utils/isa";
 import { TYPE } from "metabase-lib/types/constants";
-import Question from "metabase-lib/Question";
 import TemplateTagVariable from "metabase-lib/variables/TemplateTagVariable";
 import { TemplateTagDimension } from "metabase-lib/Dimension";
 
@@ -76,14 +75,13 @@ export function getTargetsWithSourceFilters({
   isAction,
   dashcard,
   object,
-  metadata,
 }) {
   if (isAction) {
     return getTargetsForAction(object);
   }
   return isDash
     ? getTargetsForDashboard(object, dashcard)
-    : getTargetsForQuestion(object, metadata);
+    : getTargetsForQuestion(object);
 }
 
 function getTargetsForAction(action) {
@@ -106,8 +104,8 @@ function getTargetsForAction(action) {
   });
 }
 
-function getTargetsForQuestion(question, metadata) {
-  const query = new Question(question, metadata).query();
+function getTargetsForQuestion(question) {
+  const query = question.query();
   return query
     .dimensionOptions()
     .all()
diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts
index 09612cf90e7e9b98dfa0a98c5d81e375587abf78..18cffba6c6176f478c17fac152572d28ce2ee803 100644
--- a/frontend/src/metabase-types/api/card.ts
+++ b/frontend/src/metabase-types/api/card.ts
@@ -129,3 +129,23 @@ export interface ModerationReview {
 
 export type CardId = number;
 export type ModerationReviewStatus = "verified";
+
+export type CardFilterOption =
+  | "all"
+  | "mine"
+  | "bookmarked"
+  | "database"
+  | "table"
+  | "recent"
+  | "popular"
+  | "using_model"
+  | "archived";
+
+export interface CardQuery {
+  ignore_view?: boolean;
+}
+
+export interface CardListQuery {
+  f?: CardFilterOption;
+  model_id?: CardId;
+}
diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx
index 85622d34631cde942e89753040ce9d2de30bbf7c..19f7f1ff8944124432f16f17eec56fe9511ed020 100644
--- a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx
@@ -14,7 +14,6 @@ import Questions from "metabase/entities/questions";
 import { getMetadata } from "metabase/selectors/metadata";
 
 import type {
-  Card,
   CardId,
   DatabaseId,
   WritebackActionId,
@@ -47,11 +46,10 @@ interface ActionLoaderProps {
 }
 
 interface ModelLoaderProps {
-  modelCard: Card;
+  model?: Question;
 }
 
 interface StateProps {
-  model: Question;
   metadata: Metadata;
 }
 
@@ -68,8 +66,7 @@ type Props = OwnProps &
   StateProps &
   DispatchProps;
 
-const mapStateToProps = (state: State, { modelCard }: ModelLoaderProps) => ({
-  model: new Question(modelCard, getMetadata(state)),
+const mapStateToProps = (state: State) => ({
   metadata: getMetadata(state),
 });
 
@@ -98,7 +95,7 @@ function ActionCreator({
 
   const [isSaveModalShown, setShowSaveModal] = useState(false);
 
-  const isEditable = isNew || model.canWriteActions();
+  const isEditable = isNew || (model != null && model.canWriteActions());
 
   const handleCreate = async (values: CreateActionFormValues) => {
     if (action.type !== "query") {
@@ -124,7 +121,7 @@ function ActionCreator({
     if (isSavedAction(action)) {
       const reduxAction = await onUpdateAction({
         ...action,
-        model_id: model.id(),
+        model_id: model?.id(),
         visualization_settings: formSettings,
       });
       const updatedAction = Actions.HACK_getObjectFromAction(reduxAction);
@@ -170,7 +167,7 @@ function ActionCreator({
             initialValues={{
               name: action.name,
               description: action.description,
-              model_id: model.id(),
+              model_id: model?.id(),
             }}
             onCreate={handleCreate}
             onCancel={handleCloseNewActionModal}
@@ -218,7 +215,7 @@ export default _.compose(
   }),
   Questions.load({
     id: (state: State, props: OwnProps) => props?.modelId,
-    entityAlias: "modelCard",
+    entityAlias: "model",
   }),
   Database.loadList(),
   connect(mapStateToProps, mapDispatchToProps),
diff --git a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx
index 662c77e357102c3ba0a3c1ace1442a8fc5590d13..99acb76099e3db0a5ea5ffca26030dfa30b7ff48 100644
--- a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx
@@ -9,8 +9,9 @@ import Actions from "metabase/entities/actions";
 import Models from "metabase/entities/questions";
 import { setErrorPage } from "metabase/redux/app";
 
-import type { Card, WritebackAction } from "metabase-types/api";
+import type { WritebackAction } from "metabase-types/api";
 import type { AppErrorDescriptor, State } from "metabase-types/store";
+import Question from "metabase-lib/Question";
 
 import ActionCreator from "../ActionCreator";
 
@@ -25,7 +26,7 @@ interface OwnProps {
 
 interface EntityLoaderProps {
   action?: WritebackAction;
-  model: Card;
+  model: Question;
   loading?: boolean;
 }
 
@@ -52,7 +53,7 @@ function ActionCreatorModal({
 }: ActionCreatorModalProps) {
   const actionId = Urls.extractEntityId(params.actionId);
   const modelId = Urls.extractEntityId(params.slug);
-  const databaseId = model.database_id || model.dataset_query.database;
+  const databaseId = model.databaseId();
 
   useEffect(() => {
     if (loading === false) {
@@ -60,7 +61,7 @@ function ActionCreatorModal({
       const hasModelMismatch = action != null && action.model_id !== modelId;
 
       if (notFound || action?.archived) {
-        const nextLocation = Urls.modelDetail(model, "actions");
+        const nextLocation = Urls.modelDetail(model.card(), "actions");
         onChangeLocation(nextLocation);
       } else if (hasModelMismatch) {
         setErrorPage({ status: 404 });
diff --git a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx
index 7f75e860cf2f3fd3b6fbf1062496964a9f332ae0..b92d6261fc7dd5a3202cb22d8588c9e74f9333d0 100644
--- a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx
+++ b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx
@@ -10,7 +10,6 @@ import PinnedItemSortDropTarget from "metabase/collections/components/PinnedItem
 import { isPreviewShown, isRootCollection } from "metabase/collections/utils";
 import PinDropZone from "metabase/collections/components/PinDropZone";
 import ItemDragSource from "metabase/containers/dnd/ItemDragSource";
-import Metadata from "metabase-lib/metadata/Metadata";
 import Database from "metabase-lib/metadata/Database";
 
 import {
@@ -27,7 +26,6 @@ type Props = {
   deleteBookmark: (id: string, collection: string) => void;
   items: CollectionItem[];
   collection: Collection;
-  metadata: Metadata;
   onCopy: (items: CollectionItem[]) => void;
   onMove: (items: CollectionItem[]) => void;
 };
@@ -39,7 +37,6 @@ function PinnedItemOverview({
   deleteBookmark,
   items,
   collection,
-  metadata,
   onCopy,
   onMove,
 }: Props) {
@@ -74,7 +71,6 @@ function PinnedItemOverview({
                     <div>
                       <PinnedQuestionCard
                         item={item}
-                        metadata={metadata}
                         collection={collection}
                         databases={databases}
                         bookmarks={bookmarks}
diff --git a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx
index 77f9a0c1338e07084fdbb1caac3a14a3f746c5bf..a564fab4d279da96d15e5dee02ab28cf311f7333 100644
--- a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx
+++ b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx
@@ -6,7 +6,6 @@ import {
 } from "metabase/collections/utils";
 import Visualization from "metabase/visualizations/components/Visualization";
 import { Bookmark, Collection, CollectionItem } from "metabase-types/api";
-import Metadata from "metabase-lib/metadata/Metadata";
 import Database from "metabase-lib/metadata/Database";
 import PinnedQuestionLoader from "./PinnedQuestionLoader";
 import {
@@ -19,7 +18,6 @@ import {
 export interface PinnedQuestionCardProps {
   item: CollectionItem;
   collection: Collection;
-  metadata: Metadata;
   databases?: Database[];
   bookmarks?: Bookmark[];
   onCopy: (items: CollectionItem[]) => void;
@@ -31,7 +29,6 @@ export interface PinnedQuestionCardProps {
 const PinnedQuestionCard = ({
   item,
   collection,
-  metadata,
   databases,
   bookmarks,
   onCopy,
@@ -54,7 +51,7 @@ const PinnedQuestionCard = ({
         deleteBookmark={onDeleteBookmark}
       />
       {isPreview ? (
-        <PinnedQuestionLoader id={item.id} metadata={metadata}>
+        <PinnedQuestionLoader id={item.id}>
           {({ question, rawSeries, loading, error, errorIcon }) =>
             loading ? (
               <CardPreviewSkeleton
diff --git a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx
index f535cb0c111edbaab157d84fa638973a36a522fc..fee51ba6c49ad317d24865ad52a027cd6f4ac21e 100644
--- a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx
+++ b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx
@@ -5,12 +5,10 @@ import {
   getGenericErrorMessage,
   getPermissionErrorMessage,
 } from "metabase/visualizations/lib/errors";
-import Metadata from "metabase-lib/metadata/Metadata";
 import Question from "metabase-lib/Question";
 
 export interface PinnedQuestionLoaderProps {
   id: number;
-  metadata: Metadata;
   children: (props: PinnedQuestionChildrenProps) => JSX.Element;
 }
 
@@ -24,7 +22,7 @@ export interface PinnedQuestionChildrenProps {
 
 export interface QuestionLoaderProps {
   loading: boolean;
-  question: any;
+  question: Question;
 }
 
 export interface QuestionResultLoaderProps {
@@ -37,19 +35,18 @@ export interface QuestionResultLoaderProps {
 
 const PinnedQuestionLoader = ({
   id,
-  metadata,
   children,
 }: PinnedQuestionLoaderProps): JSX.Element => {
   const questionRef = useRef<Question>();
 
   return (
     <Questions.Loader id={id} loadingAndErrorWrapper={false}>
-      {({ loading, question: card }: QuestionLoaderProps) => {
-        if (loading || !card.dataset_query) {
+      {({ loading, question: loadedQuestion }: QuestionLoaderProps) => {
+        if (loading || !loadedQuestion.query()) {
           return children({ loading: true });
         }
 
-        const question = questionRef.current ?? new Question(card, metadata);
+        const question = questionRef.current ?? loadedQuestion;
         questionRef.current = question;
 
         return (
diff --git a/frontend/src/metabase/collections/containers/CollectionContent.jsx b/frontend/src/metabase/collections/containers/CollectionContent.jsx
index 4f2dee99540ed802398975db9bdfcc78a631d8cf..cca66c11025bd034d60461ef345134d88a84a107 100644
--- a/frontend/src/metabase/collections/containers/CollectionContent.jsx
+++ b/frontend/src/metabase/collections/containers/CollectionContent.jsx
@@ -10,7 +10,6 @@ import Collection from "metabase/entities/collections";
 import Search from "metabase/entities/search";
 
 import { getUserIsAdmin } from "metabase/selectors/user";
-import { getMetadata } from "metabase/selectors/metadata";
 import { getIsBookmarked } from "metabase/collections/selectors";
 import { getSetting } from "metabase/selectors/settings";
 import { getIsNavbarOpen, openNavbar } from "metabase/redux/app";
@@ -66,7 +65,6 @@ function mapStateToProps(state, props) {
   return {
     isAdmin: getUserIsAdmin(state),
     isBookmarked: getIsBookmarked(state, props),
-    metadata: getMetadata(state),
     isNavbarOpen: getIsNavbarOpen(state),
     uploadsEnabled: canAccessUploadsDb,
   };
@@ -88,7 +86,6 @@ function CollectionContent({
   createBookmark,
   deleteBookmark,
   isAdmin,
-  metadata,
   isNavbarOpen,
   openNavbar,
   uploadFile,
@@ -263,7 +260,6 @@ function CollectionContent({
                   deleteBookmark={deleteBookmark}
                   items={pinnedItems}
                   collection={collection}
-                  metadata={metadata}
                   onMove={handleMove}
                   onCopy={handleCopy}
                   onToggleSelected={toggleItem}
diff --git a/frontend/src/metabase/common/hooks/index.ts b/frontend/src/metabase/common/hooks/index.ts
index 4875b686164a9e1471096fe653f434fc1d582dc5..0feb14e2cdfa656293d9f34e604d5331ca3035d5 100644
--- a/frontend/src/metabase/common/hooks/index.ts
+++ b/frontend/src/metabase/common/hooks/index.ts
@@ -3,6 +3,8 @@ export * from "./use-database-list-query";
 export * from "./use-database-query";
 export * from "./use-metric-list-query";
 export * from "./use-metric-query";
+export * from "./use-question-list-query";
+export * from "./use-question-query";
 export * from "./use-schema-list-query";
 export * from "./use-segment-list-query";
 export * from "./use-segment-query";
diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/index.ts b/frontend/src/metabase/common/hooks/use-question-list-query/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0005e0b915fc18e5781cbe8a906a6a1471f6fff
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-list-query/index.ts
@@ -0,0 +1 @@
+export * from "./use-question-list-query";
diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ed7f8da7d20cf40637bb7950571db3e0a6c0427c
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts
@@ -0,0 +1,19 @@
+import Questions from "metabase/entities/questions";
+import {
+  useEntityListQuery,
+  UseEntityListQueryProps,
+  UseEntityListQueryResult,
+} from "metabase/common/hooks/use-entity-list-query";
+import { CardListQuery } from "metabase-types/api";
+import Question from "metabase-lib/Question";
+
+export const useQuestionListQuery = (
+  props: UseEntityListQueryProps<CardListQuery> = {},
+): UseEntityListQueryResult<Question> => {
+  return useEntityListQuery(props, {
+    fetchList: Questions.actions.fetchList,
+    getList: Questions.selectors.getList,
+    getLoading: Questions.selectors.getLoading,
+    getError: Questions.selectors.getError,
+  });
+};
diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b2661b4a81547e983da6c2a98eaf701dbd40cce0
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import { createMockCard } from "metabase-types/api/mocks";
+import { setupCardsEndpoints } from "__support__/server-mocks";
+import {
+  renderWithProviders,
+  screen,
+  waitForElementToBeRemoved,
+} from "__support__/ui";
+import { useQuestionListQuery } from "./use-question-list-query";
+
+const TEST_CARD = createMockCard();
+
+const TestComponent = () => {
+  const { data = [], isLoading, error } = useQuestionListQuery();
+
+  if (isLoading || error) {
+    return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
+  }
+
+  return (
+    <div>
+      {data.map(question => (
+        <div key={question.id()}>{question.displayName()}</div>
+      ))}
+    </div>
+  );
+};
+
+const setup = () => {
+  setupCardsEndpoints([TEST_CARD]);
+  renderWithProviders(<TestComponent />);
+};
+
+describe("useQuestionListQuery", () => {
+  it("should be initially loading", () => {
+    setup();
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+  });
+
+  it("should show data from the response", async () => {
+    setup();
+    await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
+    expect(screen.getByText(TEST_CARD.name)).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/common/hooks/use-question-query/index.ts b/frontend/src/metabase/common/hooks/use-question-query/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a87e38417b3829c7e32f8bd7c3d3fee0e69a6562
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-query/index.ts
@@ -0,0 +1 @@
+export * from "./use-question-query";
diff --git a/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..82761274f9fbb9678db93142c4f5f6883942a7af
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts
@@ -0,0 +1,19 @@
+import Questions from "metabase/entities/questions";
+import {
+  useEntityQuery,
+  UseEntityQueryProps,
+  UseEntityQueryResult,
+} from "metabase/common/hooks/use-entity-query";
+import { CardId, CardQuery } from "metabase-types/api";
+import Question from "metabase-lib/Question";
+
+export const useQuestionQuery = (
+  props: UseEntityQueryProps<CardId, CardQuery>,
+): UseEntityQueryResult<Question> => {
+  return useEntityQuery(props, {
+    fetch: Questions.actions.fetch,
+    getObject: Questions.selectors.getObject,
+    getLoading: Questions.selectors.getLoading,
+    getError: Questions.selectors.getError,
+  });
+};
diff --git a/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4e8d0922ef42a54af535bb8dd7d99c536d872446
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import { createMockCard } from "metabase-types/api/mocks";
+import { setupCardsEndpoints } from "__support__/server-mocks";
+import {
+  renderWithProviders,
+  screen,
+  waitForElementToBeRemoved,
+} from "__support__/ui";
+import { useQuestionQuery } from "./use-question-query";
+
+const TEST_CARD = createMockCard();
+
+const TestComponent = () => {
+  const { data, isLoading, error } = useQuestionQuery({
+    id: TEST_CARD.id,
+  });
+
+  if (isLoading || error || !data) {
+    return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
+  }
+
+  return <div>{data.displayName()}</div>;
+};
+
+const setup = () => {
+  setupCardsEndpoints([TEST_CARD]);
+  renderWithProviders(<TestComponent />);
+};
+
+describe("useQuestionQuery", () => {
+  it("should be initially loading", () => {
+    setup();
+    expect(screen.getByText("Loading...")).toBeInTheDocument();
+  });
+
+  it("should show data from the response", async () => {
+    setup();
+    await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
+    expect(screen.getByText(TEST_CARD.name)).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/containers/SavedQuestionLoader.jsx b/frontend/src/metabase/containers/SavedQuestionLoader.jsx
index f454e59f7ca74a3e24119789b6a7ab76df8cd807..fd6f754adb92e18ab3729119ad4698742ce1e077 100644
--- a/frontend/src/metabase/containers/SavedQuestionLoader.jsx
+++ b/frontend/src/metabase/containers/SavedQuestionLoader.jsx
@@ -36,21 +36,26 @@ import Question from "metabase-lib/Question";
  *
  * @example
  */
-const SavedQuestionLoader = ({ children, card, error, loading }) => {
+const SavedQuestionLoader = ({
+  children,
+  question: loadedQuestion,
+  error,
+  loading,
+}) => {
   const metadata = useSelector(getMetadata);
   const dispatch = useDispatch();
   const [question, setQuestion] = useState(null);
 
   const cardMetadataState = useAsync(async () => {
-    if (card?.id == null) {
+    if (loadedQuestion?.id() == null) {
       return;
     }
 
-    await dispatch(loadMetadataForCard(card));
-  }, [card?.id]);
+    await dispatch(loadMetadataForCard(loadedQuestion.card()));
+  }, [loadedQuestion?.id()]);
 
   useEffect(() => {
-    if (card?.id == null) {
+    if (loadedQuestion?.id() == null) {
       return;
     }
 
@@ -63,9 +68,9 @@ const SavedQuestionLoader = ({ children, card, error, loading }) => {
     }
 
     if (!question) {
-      setQuestion(new Question(card, metadata));
+      setQuestion(new Question(loadedQuestion.card(), metadata));
     }
-  }, [card, metadata, cardMetadataState, question]);
+  }, [loadedQuestion, metadata, cardMetadataState, question]);
 
   return (
     children?.({
@@ -80,6 +85,5 @@ export default _.compose(
   Questions.load({
     id: (_state, props) => props.questionId,
     loadingAndErrorWrapper: false,
-    entityAlias: "card",
   }),
 )(SavedQuestionLoader);
diff --git a/frontend/src/metabase/dashboard/actions/cards.js b/frontend/src/metabase/dashboard/actions/cards.js
index fe78bd94b9598f69e28fd5f235fa0ff6f897aa6b..74fe0ce976fd457b4ab4ad6fb5eece9be53994ea 100644
--- a/frontend/src/metabase/dashboard/actions/cards.js
+++ b/frontend/src/metabase/dashboard/actions/cards.js
@@ -27,9 +27,9 @@ export const addCardToDashboard =
   ({ dashId, cardId, tabId }) =>
   async (dispatch, getState) => {
     await dispatch(Questions.actions.fetch({ id: cardId }));
-    const card = Questions.selectors.getObject(getState(), {
-      entityId: cardId,
-    });
+    const card = Questions.selectors
+      .getObject(getState(), { entityId: cardId })
+      .card();
     const { dashboards, dashcards } = getState().dashboard;
     const dashboard = dashboards[dashId];
     const existingCards = dashboard.ordered_cards
diff --git a/frontend/src/metabase/dashboard/actions/metadata.js b/frontend/src/metabase/dashboard/actions/metadata.js
index 5d39e9547528e01a362fe44b14152297e89f26fb..5291eecf64bf836de02d16476867144d6c326c6b 100644
--- a/frontend/src/metabase/dashboard/actions/metadata.js
+++ b/frontend/src/metabase/dashboard/actions/metadata.js
@@ -47,7 +47,7 @@ const loadMetadataForLinkedTargets =
     const cards = linkTargets
       .filter(({ entityType }) => entityType === "question")
       .map(({ entityId }) =>
-        Questions.selectors.getObject(getState(), { entityId }),
+        Questions.selectors.getObject(getState(), { entityId })?.card(),
       )
       .filter(card => card != null);
 
diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
index 619bf81dcc18a8ac61eb783d4aab9f572d73b55f..87a7a5a65bac5dabbb7b754005b9647cc6a90abf 100644
--- a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
@@ -4,7 +4,6 @@ import PropTypes from "prop-types";
 import { t } from "ttag";
 import { getIn } from "icepick";
 import { connect } from "react-redux";
-import { createSelector } from "@reduxjs/toolkit";
 import _ from "underscore";
 
 import Visualization from "metabase/visualizations/components/Visualization";
@@ -13,20 +12,12 @@ import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { color } from "metabase/lib/colors";
 
 import Questions from "metabase/entities/questions";
-import { getMetadataWithHiddenTables } from "metabase/selectors/metadata";
 import { loadMetadataForQueries } from "metabase/redux/metadata";
 
 import { getVisualizationRaw } from "metabase/visualizations";
-import Question from "metabase-lib/Question";
 
 import { QuestionList } from "./QuestionList";
 
-const getQuestions = createSelector(
-  [getMetadataWithHiddenTables, (_state, props) => props.questions],
-  (metadata, questions) =>
-    questions && questions.map(card => new Question(card, metadata)),
-);
-
 // TODO: rework this so we don't have to load all cards up front
 
 class AddSeriesModal extends Component {
@@ -239,11 +230,9 @@ class AddSeriesModal extends Component {
 }
 
 export default _.compose(
-  Questions.loadList({ query: { f: "all" } }),
-  connect(
-    (state, ownProps) => ({
-      questions: getQuestions(state, ownProps),
-    }),
-    { loadMetadataForQueries },
-  ),
+  Questions.loadList({
+    query: { f: "all" },
+    selectorName: "getListUnfiltered",
+  }),
+  connect(null, { loadMetadataForQueries }),
 )(AddSeriesModal);
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx
index 471e427ac30d2acb4c5b49796d419af947ec5732..fd7ec07b727aad49daa0af7a3967a8b39f86f6d7 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx
@@ -19,11 +19,11 @@ import type {
   Dashboard,
   DashboardId,
   DashboardOrderedCard,
-  Card,
   CardId,
   ClickBehavior,
   EntityCustomDestinationClickBehavior,
 } from "metabase-types/api";
+import Question from "metabase-lib/Question";
 
 import { SidebarItem } from "../SidebarItem";
 import { Heading } from "../ClickBehaviorSidebar.styled";
@@ -82,7 +82,7 @@ function PickerControl({
   );
 }
 
-function getTargetClickMappingsHeading(entity: Card | Dashboard) {
+function getTargetClickMappingsHeading(entity: Question | Dashboard) {
   return {
     dashboard: t`Pass values to this dashboard's filters (optional)`,
     native: t`Pass values to this question's variables (optional)`,
@@ -104,7 +104,7 @@ function TargetClickMappings({
   const Entity = isDash ? Dashboards : Questions;
   return (
     <Entity.Loader id={clickBehavior.targetId}>
-      {({ object }: { object: Card | Dashboard }) => (
+      {({ object }: { object: Question | Dashboard }) => (
         <div className="pt1">
           <Heading>{getTargetClickMappingsHeading(object)}</Heading>
           <ClickMappings
diff --git a/frontend/src/metabase/dashboard/components/ClickMappings.jsx b/frontend/src/metabase/dashboard/components/ClickMappings.jsx
index 3e51acf75878a2bb40540626223b9ebc054e568c..6fbad9ab509e3041a6efa42927c49734a62692ff 100644
--- a/frontend/src/metabase/dashboard/components/ClickMappings.jsx
+++ b/frontend/src/metabase/dashboard/components/ClickMappings.jsx
@@ -13,7 +13,6 @@ import { isPivotGroupColumn } from "metabase/lib/data_grid";
 import { GTAPApi } from "metabase/services";
 
 import { loadMetadataForQuery } from "metabase/redux/metadata";
-import { getMetadata } from "metabase/selectors/metadata";
 import { getParameters } from "metabase/dashboard/selectors";
 import { getTargetsWithSourceFilters } from "metabase-lib/parameters/utils/click-behavior";
 import Question from "metabase-lib/Question";
@@ -120,7 +119,6 @@ const ClickMappings = _.compose(
   withUserAttributes,
   connect((state, props) => {
     const { object, isDash, dashcard, clickBehavior } = props;
-    const metadata = getMetadata(state, props);
     let parameters = getParameters(state, props);
 
     if (props.excludeParametersSources) {
@@ -142,7 +140,6 @@ const ClickMappings = _.compose(
         isDash,
         dashcard,
         object,
-        metadata,
       }),
       ({ id }) =>
         getIn(clickBehavior, ["parameterMapping", id, "source"]) != null,
@@ -286,9 +283,9 @@ function loadQuestionMetadata(getQuestion) {
       }
 
       fetch() {
-        const { question, metadata, loadMetadataForQuery } = this.props;
+        const { question, loadMetadataForQuery } = this.props;
         if (question) {
-          loadMetadataForQuery(new Question(question, metadata).query());
+          loadMetadataForQuery(question.query());
         }
       }
 
@@ -301,7 +298,6 @@ function loadQuestionMetadata(getQuestion) {
 
     return connect(
       (state, props) => ({
-        metadata: getMetadata(state),
         question: getQuestion && getQuestion(state, props),
       }),
       { loadMetadataForQuery },
@@ -335,9 +331,9 @@ export function isMappableColumn(column) {
 }
 
 export function clickTargetObjectType(object) {
-  if (!object.dataset_query) {
+  if (!(object instanceof Question)) {
     return "dashboard";
-  } else if (new Question(object).isNative()) {
+  } else if (object.isNative()) {
     return "native";
   } else {
     return "gui";
diff --git a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js b/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
index ac6d20cddda21fa832cd42c522bb94b8e26a4223..64edefa938f2f0b6b7c4d4d90938db089eb190b0 100644
--- a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
+++ b/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js
@@ -17,8 +17,10 @@ const WithVizSettingsData = ComposedComponent => {
             .groupBy(target => target.entity.name)
             .mapObject(targets =>
               _.chain(targets)
-                .map(({ entity, entityId }) =>
-                  entity.selectors.getObject(state, { entityId }),
+                .map(({ entity, entityType, entityId }) =>
+                  entityType === "question"
+                    ? entity.selectors.getObject(state, { entityId })?.card()
+                    : entity.selectors.getObject(state, { entityId }),
                 )
                 .filter(object => object != null)
                 .indexBy(object => object.id)
diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js
index 5999db91f6f112a4c551a6a332c7fb4543b8e45c..01eb1b394e87f93d01f2fd1474c2ab834bccab11 100644
--- a/frontend/src/metabase/entities/questions.js
+++ b/frontend/src/metabase/entities/questions.js
@@ -4,18 +4,21 @@ import { updateIn } from "icepick";
 import { createEntity, undo } from "metabase/lib/entities";
 import * as Urls from "metabase/lib/urls";
 import { color } from "metabase/lib/colors";
+import {
+  getMetadata,
+  getMetadataUnfiltered,
+} from "metabase/selectors/metadata";
 
 import Collections, {
   getCollectionType,
   normalizedCollection,
 } from "metabase/entities/collections";
-
 import {
   API_UPDATE_QUESTION,
   SOFT_RELOAD_CARD,
 } from "metabase/query_builder/actions";
-import { canonicalCollectionId } from "metabase/collections/utils";
 
+import { canonicalCollectionId } from "metabase/collections/utils";
 import forms from "./questions/forms";
 
 const Questions = createEntity({
@@ -83,6 +86,19 @@ const Questions = createEntity({
       Questions.actions.update({ id }, { collection_preview }, opts),
   },
 
+  selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).question(entityId),
+    getObjectUnfiltered: (state, { entityId }) =>
+      getMetadataUnfiltered(state).question(entityId),
+    getListUnfiltered: (state, { entityQuery }) => {
+      const entityIds =
+        Questions.selectors.getEntityIds(state, { entityQuery }) ?? [];
+      return entityIds.map(entityId =>
+        Questions.selectors.getObjectUnfiltered(state, { entityId }),
+      );
+    },
+  },
+
   objectSelectors: {
     getName: question => question && question.name,
     getUrl: (question, opts) => question && Urls.question(question, opts),
diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js
index 49a0882a15363b80a4d7edb6da15b64839c5b1ca..7f811d407fbf6183e22e64908dc10efa50f632c6 100644
--- a/frontend/src/metabase/lib/card.js
+++ b/frontend/src/metabase/lib/card.js
@@ -27,10 +27,10 @@ export function startNewCard(type, databaseId, tableId) {
 export async function loadCard(cardId, { dispatch, getState }) {
   try {
     await dispatch(Questions.actions.fetch({ id: cardId }, { reload: true }));
-    const card = Questions.selectors.getObject(getState(), {
+    const question = Questions.selectors.getObject(getState(), {
       entityId: cardId,
     });
-    return card;
+    return question?.card();
   } catch (error) {
     console.error("error loading card", error);
     throw error;
diff --git a/frontend/src/metabase/lib/entities.js b/frontend/src/metabase/lib/entities.js
index 2610d56e66878b0af936913f7a2231ed276d9eaa..ab0d9ae9cae972fa4ab7e102039593b968896827 100644
--- a/frontend/src/metabase/lib/entities.js
+++ b/frontend/src/metabase/lib/entities.js
@@ -243,7 +243,7 @@ export function createEntity(def) {
       (entityObject, updatedObject = null, { notify } = {}) =>
         async (dispatch, getState) => {
           // save the original object for undo
-          const originalObject = entity.selectors.getObject(getState(), {
+          const originalObject = getObject(getState(), {
             entityId: entityObject.id,
           });
           // If a second object is provided just take the id from the first and
diff --git a/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx b/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx
index 72d14052436d4ba9a1a1ee3e41ec4bfa790946f4..10c56ba5df670979fe5d02419d9d877efcf86af4 100644
--- a/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx
+++ b/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx
@@ -8,8 +8,7 @@ import Databases from "metabase/entities/databases";
 import Questions from "metabase/entities/questions";
 import Search from "metabase/entities/search";
 import { getUser } from "metabase/selectors/user";
-import { getMetadata } from "metabase/selectors/metadata";
-import { Card, CollectionItem, DatabaseId, User } from "metabase-types/api";
+import { CollectionItem, DatabaseId, User } from "metabase-types/api";
 import { Dispatch, State } from "metabase-types/store";
 import { canUseMetabotOnDatabase } from "metabase/metabot/utils";
 import Question from "metabase-lib/Question";
@@ -28,12 +27,11 @@ interface SearchLoaderProps {
 }
 
 interface CardLoaderProps {
-  card?: Card;
+  model?: Question;
 }
 
 interface StateProps {
   user: User | null;
-  model: Question | null;
   databases: Database[];
 }
 
@@ -41,14 +39,16 @@ interface DispatchProps {
   onSubmitQuery: (databaseId: DatabaseId, query: string) => void;
 }
 
-type MetabotWidgetProps = StateProps & DispatchProps & DatabaseLoaderProps;
+type MetabotWidgetProps = StateProps &
+  DispatchProps &
+  CardLoaderProps &
+  DatabaseLoaderProps;
 
 const mapStateToProps = (
   state: State,
-  { card, databases }: CardLoaderProps & DatabaseLoaderProps,
+  { databases }: DatabaseLoaderProps,
 ): StateProps => ({
   user: getUser(state),
-  model: card ? new Question(card, getMetadata(state)) : null,
   databases: databases.filter(canUseMetabotOnDatabase),
 });
 
@@ -103,7 +103,7 @@ const getGreetingMessage = (user: User | null) => {
   }
 };
 
-const getPromptPlaceholder = (model: Question | null) => {
+const getPromptPlaceholder = (model: Question | undefined) => {
   if (model) {
     return t`Ask something like, how many ${model?.displayName()} have we had over time?`;
   } else {
@@ -122,7 +122,7 @@ export default _.compose(
   }),
   Questions.load({
     id: (state: State, { models }: SearchLoaderProps) => models[0]?.id,
-    entityAlias: "card",
+    entityAlias: "model",
   }),
   Databases.loadList(),
   connect(mapStateToProps, mapDispatchToProps),
diff --git a/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx b/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx
index 7b396cdabe4da123bcae19c136171eb532042d18..cfa8e246911a2fbd63781bf2b826048846de802e 100644
--- a/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx
+++ b/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx
@@ -3,9 +3,8 @@ import _ from "underscore";
 import { LocationDescriptorObject } from "history";
 import { checkNotNull } from "metabase/core/utils/types";
 import { extractEntityId } from "metabase/lib/urls";
-import { getMetadata } from "metabase/selectors/metadata";
 import Questions from "metabase/entities/questions";
-import { Card, CardId } from "metabase-types/api";
+import { CardId } from "metabase-types/api";
 import { MetabotEntityType, State } from "metabase-types/store";
 import Question from "metabase-lib/Question";
 import Metabot from "../../components/Metabot";
@@ -20,26 +19,24 @@ interface RouteProps {
 }
 
 interface CardLoaderProps {
-  card: Card;
+  model: Question;
 }
 
 interface StateProps {
   entityId: CardId;
   entityType: MetabotEntityType;
-  model: Question;
   initialPrompt?: string;
 }
 
 const mapStateToProps = (
   state: State,
-  { card, params, location }: CardLoaderProps & RouteProps,
+  { params, location }: CardLoaderProps & RouteProps,
 ): StateProps => {
   const entityId = checkNotNull(extractEntityId(params.slug));
 
   return {
     entityId,
     entityType: "model",
-    model: new Question(card, getMetadata(state)),
     initialPrompt: location?.query?.prompt,
   };
 };
@@ -48,7 +45,7 @@ const mapStateToProps = (
 export default _.compose(
   Questions.load({
     id: (state: State, { params }: RouteProps) => extractEntityId(params.slug),
-    entityAlias: "card",
+    entityAlias: "model",
   }),
   connect(mapStateToProps),
 )(Metabot);
diff --git a/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx b/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx
index 543561459599dd050238611325310d0187155784..ce70b1cc126492759f1d358afde4adf0431d8ddf 100644
--- a/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx
+++ b/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx
@@ -10,7 +10,6 @@ import Questions, {
   getIcon as getQuestionIcon,
 } from "metabase/entities/questions";
 
-import type { Card } from "metabase-types/api";
 import type { State } from "metabase-types/store";
 import type Question from "metabase-lib/Question";
 
@@ -28,13 +27,13 @@ interface OwnProps {
 }
 
 interface EntityLoaderProps {
-  cards: Card[];
+  questions: Question[];
 }
 
 type Props = OwnProps & EntityLoaderProps;
 
-function ModelUsageDetails({ model, cards, hasNewQuestionLink }: Props) {
-  if (cards.length === 0) {
+function ModelUsageDetails({ model, questions, hasNewQuestionLink }: Props) {
+  if (questions.length === 0) {
     return (
       <EmptyStateContainer>
         <EmptyStateTitle>{t`This model is not used by any questions yet.`}</EmptyStateTitle>
@@ -53,11 +52,14 @@ function ModelUsageDetails({ model, cards, hasNewQuestionLink }: Props) {
 
   return (
     <ul>
-      {cards.map(card => (
-        <li key={card.id}>
-          <CardListItem to={Urls.question(card)} aria-label={card.name}>
-            <Icon name={getQuestionIcon(card).name} />
-            <CardTitle>{card.name}</CardTitle>
+      {questions.map(question => (
+        <li key={question.id()}>
+          <CardListItem
+            to={Urls.question(question.card())}
+            aria-label={question.displayName() ?? ""}
+          >
+            <Icon name={getQuestionIcon(question.card()).name} />
+            <CardTitle>{question.displayName()}</CardTitle>
           </CardListItem>
         </li>
       ))}
@@ -74,6 +76,5 @@ function getCardListingQuery(state: State, { model }: OwnProps) {
 
 // eslint-disable-next-line import/no-default-export -- deprecated usage
 export default Questions.loadList({
-  listName: "cards",
   query: getCardListingQuery,
 })(ModelUsageDetails);
diff --git a/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx b/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx
index 3b4a2abe885a543fa96c2f6410e8d552911bd417..5d8b7f90f723452dc53040f4d014adfbf5ccfa3b 100644
--- a/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx
+++ b/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx
@@ -13,7 +13,6 @@ import Actions from "metabase/entities/actions";
 import Databases from "metabase/entities/databases";
 import Questions from "metabase/entities/questions";
 import Tables from "metabase/entities/tables";
-import { getMetadata } from "metabase/selectors/metadata";
 import title from "metabase/hoc/Title";
 
 import { loadMetadataForCard } from "metabase/questions/actions";
@@ -38,10 +37,6 @@ type OwnProps = {
 
 type EntityLoadersProps = {
   actions: WritebackAction[];
-  modelCard: Card;
-};
-
-type StateProps = {
   model: Question;
 };
 
@@ -64,13 +59,7 @@ type DispatchProps = {
   onChangeLocation: (location: LocationDescriptor) => void;
 };
 
-type Props = OwnProps & EntityLoadersProps & StateProps & DispatchProps;
-
-function mapStateToProps(state: State, props: OwnProps & EntityLoadersProps) {
-  const metadata = getMetadata(state);
-  const model = new Question(props.modelCard, metadata);
-  return { model };
-}
+type Props = OwnProps & EntityLoadersProps & DispatchProps;
 
 const mapDispatchToProps = {
   loadMetadataForCard,
@@ -201,21 +190,21 @@ function getModelId(state: State, props: OwnProps) {
   return Urls.extractEntityId(props.params.slug);
 }
 
-function getPageTitle({ modelCard }: Props) {
-  return modelCard?.name;
+function getPageTitle({ model }: Props) {
+  return model?.displayName();
 }
 
 // eslint-disable-next-line import/no-default-export -- deprecated usage
 export default _.compose(
-  Questions.load({ id: getModelId, entityAlias: "modelCard" }),
+  Questions.load({ id: getModelId, entityAlias: "model" }),
   Databases.loadList(),
   Actions.loadList({
     query: (state: State, props: OwnProps) => ({
       "model-id": getModelId(state, props),
     }),
   }),
-  connect<StateProps, DispatchProps, OwnProps & EntityLoadersProps, State>(
-    mapStateToProps,
+  connect<null, DispatchProps, OwnProps & EntityLoadersProps, State>(
+    null,
     mapDispatchToProps,
   ),
   title(getPageTitle),
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx
index 6f3ffb4ccbf096fb589617b9a64725c278da9d1c..0a503945ec566b7226ebb87f0fb52872d8212ee8 100644
--- a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx
+++ b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx
@@ -11,8 +11,9 @@ import Questions from "metabase/entities/questions";
 
 import { getDashboard } from "metabase/dashboard/selectors";
 
-import type { Card, Dashboard } from "metabase-types/api";
+import type { Dashboard } from "metabase-types/api";
 import type { State } from "metabase-types/store";
+import Question from "metabase-lib/Question";
 
 import MainNavbarContainer from "./MainNavbarContainer";
 
@@ -28,7 +29,7 @@ import getSelectedItems, {
 import { NavRoot, Sidebar } from "./MainNavbar.styled";
 
 interface EntityLoaderProps {
-  card?: Card;
+  question?: Question;
 }
 
 interface StateProps {
@@ -63,7 +64,7 @@ function MainNavbar({
   isOpen,
   location,
   params,
-  card,
+  question,
   dashboard,
   openNavbar,
   closeNavbar,
@@ -92,10 +93,10 @@ function MainNavbar({
       getSelectedItems({
         pathname: location.pathname,
         params,
-        card,
+        question,
         dashboard,
       }),
-    [location, params, card, dashboard],
+    [location, params, question, dashboard],
   );
 
   return (
@@ -136,6 +137,6 @@ export default _.compose(
   Questions.load({
     id: maybeGetQuestionId,
     loadingAndErrorWrapper: false,
-    entityAlias: "card",
+    entityAlias: "question",
   }),
 )(MainNavbar);
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts b/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts
index 91131cb2b0cbefec685b0694e3ae1bed68d40c0a..502ade34bd18e019cfa4794f3f72f9f917072700 100644
--- a/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts
+++ b/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts
@@ -2,7 +2,8 @@ import * as Urls from "metabase/lib/urls";
 
 import { coerceCollectionId } from "metabase/collections/utils";
 
-import type { Card, Dashboard } from "metabase-types/api";
+import type { Dashboard } from "metabase-types/api";
+import Question from "metabase-lib/Question";
 
 import { SelectedItem } from "./types";
 
@@ -12,7 +13,7 @@ type Opts = {
     slug?: string;
     pageId?: string;
   };
-  card?: Card;
+  question?: Question;
   dashboard?: Dashboard;
 };
 
@@ -39,7 +40,7 @@ function isDashboardPath(pathname: string): boolean {
 function getSelectedItems({
   pathname,
   params,
-  card,
+  question,
   dashboard,
 }: Opts): SelectedItem[] {
   const { slug } = params;
@@ -66,14 +67,14 @@ function getSelectedItems({
       },
     ];
   }
-  if ((isQuestionPath(pathname) || isModelPath(pathname)) && card) {
+  if ((isQuestionPath(pathname) || isModelPath(pathname)) && question) {
     return [
       {
-        id: card.id,
+        id: question.id(),
         type: "card",
       },
       {
-        id: coerceCollectionId(card.collection_id),
+        id: coerceCollectionId(question.collectionId()),
         type: "collection",
       },
     ];
diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx
index 0b3486b0fe6993b8bf1b3f4eea21fe56066b6ad9..42562394c6fee935d217c4b08258e2216a973ce0 100644
--- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx
+++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx
@@ -14,10 +14,8 @@ import DataPicker, {
 import Questions from "metabase/entities/questions";
 import Collections from "metabase/entities/collections";
 import Tables from "metabase/entities/tables";
-import { getMetadata } from "metabase/selectors/metadata";
 import { coerceCollectionId } from "metabase/collections/utils";
 import {
-  Card,
   CardId,
   Collection,
   Parameter,
@@ -50,27 +48,22 @@ interface ModalOwnProps {
   onClose: () => void;
 }
 
-interface ModalCardProps {
-  card: Card | undefined;
+interface ModalQuestionProps {
+  question: Question | undefined;
 }
 
 interface ModalCollectionProps {
   collection: Collection | undefined;
 }
 
-interface ModalStateProps {
-  question: Question | undefined;
-}
-
 interface ModalDispatchProps {
   onFetchCard: (cardId: CardId) => void;
   onFetchMetadata: (cardId: CardId) => void;
 }
 
 type ModalProps = ModalOwnProps &
-  ModalCardProps &
+  ModalQuestionProps &
   ModalCollectionProps &
-  ModalStateProps &
   ModalDispatchProps;
 
 const ValuesSourceCardModal = ({
@@ -181,13 +174,6 @@ const getCardIdFromValue = ({ tableIds }: DataPickerValue) => {
   }
 };
 
-const mapStateToProps = (
-  state: State,
-  { card }: ModalCardProps,
-): ModalStateProps => ({
-  question: card ? new Question(card, getMetadata(state)) : undefined,
-});
-
 const mapDispatchToProps: ModalDispatchProps = {
   onFetchCard: (cardId: CardId) => Questions.actions.fetch({ id: cardId }),
   onFetchMetadata: (cardId: CardId) =>
@@ -198,13 +184,12 @@ const mapDispatchToProps: ModalDispatchProps = {
 export default _.compose(
   Questions.load({
     id: (state: State, { sourceConfig: { card_id } }: ModalOwnProps) => card_id,
-    entityAlias: "card",
     LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper,
   }),
   Collections.load({
-    id: (state: State, { card }: ModalCardProps) =>
-      card ? coerceCollectionId(card.collection_id) : undefined,
+    id: (state: State, { question }: ModalQuestionProps) =>
+      question ? coerceCollectionId(question?.collectionId()) : undefined,
     LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper,
   }),
-  connect(mapStateToProps, mapDispatchToProps),
+  connect(null, mapDispatchToProps),
 )(ValuesSourceCardModal);
diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
index 0b0c09a1e7383b5761c1126f3c904743fec75805..8d4274776620988bf41f3151c186f4ce714acd90 100644
--- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
+++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx
@@ -19,9 +19,7 @@ import ModalContent from "metabase/components/ModalContent";
 import { useSafeAsyncFunction } from "metabase/hooks/use-safe-async-function";
 import Tables from "metabase/entities/tables";
 import Questions from "metabase/entities/questions";
-import { getMetadata } from "metabase/selectors/metadata";
 import {
-  Card,
   ValuesSourceConfig,
   ValuesSourceType,
   Parameter,
@@ -62,11 +60,7 @@ interface ModalOwnProps {
   onClose: () => void;
 }
 
-interface ModalCardProps {
-  card: Card | undefined;
-}
-
-interface ModalStateProps {
+interface ModalQuestionProps {
   question: Question | undefined;
 }
 
@@ -76,10 +70,7 @@ interface ModalDispatchProps {
   ) => Promise<ParameterValues>;
 }
 
-type ModalProps = ModalOwnProps &
-  ModalCardProps &
-  ModalStateProps &
-  ModalDispatchProps;
+type ModalProps = ModalOwnProps & ModalQuestionProps & ModalDispatchProps;
 
 const ValuesSourceTypeModal = ({
   parameter,
@@ -495,13 +486,6 @@ const useParameterValues = ({
   return state;
 };
 
-const mapStateToProps = (
-  state: State,
-  { card }: ModalOwnProps & ModalCardProps,
-): ModalStateProps => ({
-  question: card ? new Question(card, getMetadata(state)) : undefined,
-});
-
 const mapDispatchToProps = {
   onFetchParameterValues: fetchParameterValues,
 };
@@ -517,8 +501,7 @@ export default _.compose(
   }),
   Questions.load({
     id: (state: State, { sourceConfig: { card_id } }: ModalOwnProps) => card_id,
-    entityAlias: "card",
     LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper,
   }),
-  connect(mapStateToProps, mapDispatchToProps),
+  connect(null, mapDispatchToProps),
 )(ValuesSourceTypeModal);
diff --git a/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx b/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx
index 7bf8b90b90733b720bf3b0a32a05e1a953f96f22..cffd2a37f27f14afba21d99988a1cfe5f296389a 100644
--- a/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx
+++ b/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx
@@ -1,5 +1,4 @@
 import React from "react";
-import { connect } from "react-redux";
 import { t, jt } from "ttag";
 import _ from "underscore";
 
@@ -11,8 +10,6 @@ import {
 import Collections from "metabase/entities/collections";
 import Questions from "metabase/entities/questions";
 import SidebarContent from "metabase/query_builder/components/SidebarContent";
-import { getQuestionFromCard } from "metabase/query_builder/selectors";
-import type { Card } from "metabase-types/api/card";
 import type { Collection } from "metabase-types/api/collection";
 import type { State } from "metabase-types/store";
 import Table from "metabase-lib/metadata/Table";
@@ -32,15 +29,10 @@ interface QuestionPaneProps {
   onItemClick: (type: string, item: unknown) => void;
   onBack: () => void;
   onClose: () => void;
-  card: Card;
   question: Question;
   collection: Collection | null;
 }
 
-const mapStateToProps = (state: State, { card }: QuestionPaneProps) => ({
-  question: getQuestionFromCard(state, card),
-});
-
 const QuestionPane = ({
   onItemClick,
   question,
@@ -113,11 +105,10 @@ const QuestionPane = ({
 export default _.compose(
   Questions.load({
     id: (_state: State, props: QuestionPaneProps) => props.question.id,
-    entityAlias: "card",
   }),
   Collections.load({
-    id: (_state: State, props: QuestionPaneProps) => props.card.collection_id,
+    id: (_state: State, props: QuestionPaneProps) =>
+      props.question.collectionId(),
     loadingAndErrorWrapper: false,
   }),
-  connect(mapStateToProps),
 )(QuestionPane);
diff --git a/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx b/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx
index 2cb200a4d681d9f05493a25efb4b08cdda49e22b..6ddbf98be36a4dd81856f67821f13607cf86cd04 100644
--- a/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx
+++ b/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx
@@ -1,11 +1,8 @@
 import React from "react";
-import { connect } from "react-redux";
 import { t } from "ttag";
 import _ from "underscore";
 import Button from "metabase/core/components/Button";
 import Questions from "metabase/entities/questions";
-import { getMetadata } from "metabase/selectors/metadata";
-import { Card } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import Question from "metabase-lib/Question";
 import StructuredQuery from "metabase-lib/queries/StructuredQuery";
@@ -30,15 +27,11 @@ interface NotebookOwnProps {
   readOnly?: boolean;
 }
 
-interface NotebookCardProps {
-  sourceCard?: Card;
-}
-
-interface NotebookStateProps {
+interface EntityLoaderProps {
   sourceQuestion?: Question;
 }
 
-type NotebookProps = NotebookOwnProps & NotebookCardProps & NotebookStateProps;
+type NotebookProps = NotebookOwnProps & EntityLoaderProps;
 
 const Notebook = ({ className, ...props }: NotebookProps) => {
   const {
@@ -89,7 +82,7 @@ const Notebook = ({ className, ...props }: NotebookProps) => {
   );
 };
 
-function getSourceCardId(question: Question) {
+function getSourceQuestionId(question: Question) {
   const query = question.query();
   if (query instanceof StructuredQuery) {
     const sourceTableId = query.sourceTableId();
@@ -99,22 +92,12 @@ function getSourceCardId(question: Question) {
   }
 }
 
-function mapStateToProps(
-  state: State,
-  { sourceCard }: NotebookCardProps,
-): NotebookStateProps {
-  return {
-    sourceQuestion: sourceCard && new Question(sourceCard, getMetadata(state)),
-  };
-}
-
 // eslint-disable-next-line import/no-default-export -- deprecated usage
 export default _.compose(
   Questions.load({
     id: (state: State, { question }: NotebookOwnProps) =>
-      getSourceCardId(question),
-    entityAlias: "sourceCard",
+      getSourceQuestionId(question),
+    entityAlias: "sourceQuestion",
     loadingAndErrorWrapper: false,
   }),
-  connect(mapStateToProps),
 )(Notebook);
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx
index 38f72e53b7b0451432a54b64924662b6a158e8dd..ba12f498d6ca130d6962a8efc0b019417e8fbfe7 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx
@@ -4,6 +4,7 @@ import PropTypes from "prop-types";
 
 import { color } from "metabase/lib/colors";
 import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
 import Questions from "metabase/entities/questions";
 
 import Tooltip from "metabase/core/components/Tooltip";
@@ -48,7 +49,7 @@ function QuestionDataSource({ question, originalQuestion, subHead, ...props }) {
   if (originalQuestion?.id() === sourceQuestionId) {
     return (
       <SourceDatasetBreadcrumbs
-        dataset={originalQuestion.card()}
+        model={originalQuestion}
         variant={variant}
         {...props}
       />
@@ -57,23 +58,35 @@ function QuestionDataSource({ question, originalQuestion, subHead, ...props }) {
 
   return (
     <Questions.Loader id={sourceQuestionId} loadingAndErrorWrapper={false}>
-      {({ question: sourceQuestion }) => {
-        if (!sourceQuestion) {
-          return null;
-        }
-        if (sourceQuestion.dataset) {
-          return (
-            <SourceDatasetBreadcrumbs
-              dataset={sourceQuestion}
-              variant={variant}
-              {...props}
-            />
-          );
-        }
-        return (
-          <DataSourceCrumbs question={question} variant={variant} {...props} />
-        );
-      }}
+      {({ question: sourceQuestion }) => (
+        <Collections.Loader
+          id={sourceQuestion?.collectionId()}
+          loadingAndErrorWrapper={false}
+        >
+          {({ collection, loading }) => {
+            if (!sourceQuestion || loading) {
+              return null;
+            }
+            if (sourceQuestion.isDataset()) {
+              return (
+                <SourceDatasetBreadcrumbs
+                  model={sourceQuestion}
+                  collection={collection}
+                  variant={variant}
+                  {...props}
+                />
+              );
+            }
+            return (
+              <DataSourceCrumbs
+                question={question}
+                variant={variant}
+                {...props}
+              />
+            );
+          }}
+        </Collections.Loader>
+      )}
     </Questions.Loader>
   );
 }
@@ -94,11 +107,11 @@ function DataSourceCrumbs({ question, variant, isObjectDetail, ...props }) {
 }
 
 SourceDatasetBreadcrumbs.propTypes = {
-  dataset: PropTypes.object.isRequired,
+  model: PropTypes.object.isRequired,
+  collection: PropTypes.object.isRequired,
 };
 
-function SourceDatasetBreadcrumbs({ dataset, ...props }) {
-  const { collection } = dataset;
+function SourceDatasetBreadcrumbs({ model, collection, ...props }) {
   return (
     <HeadBreadcrumbs
       {...props}
@@ -111,7 +124,7 @@ function SourceDatasetBreadcrumbs({ dataset, ...props }) {
         >
           {collection?.name || t`Our analytics`}
         </HeadBreadcrumbs.Badge>,
-        dataset.archived ? (
+        model.isArchived() ? (
           <Tooltip
             key="dataset-name"
             tooltip={t`This model is archived and shouldn't be used.`}
@@ -122,15 +135,15 @@ function SourceDatasetBreadcrumbs({ dataset, ...props }) {
               inactiveColor="text-light"
               icon={{ name: "warning", color: color("danger") }}
             >
-              {dataset.name}
+              {model.displayName()}
             </HeadBreadcrumbs.Badge>
           </Tooltip>
         ) : (
           <HeadBreadcrumbs.Badge
-            to={Urls.question(dataset)}
+            to={Urls.question(model.card())}
             inactiveColor="text-light"
           >
-            {dataset.name}
+            {model.displayName()}
           </HeadBreadcrumbs.Badge>
         ),
       ]}
diff --git a/frontend/test/metabase/lib/click-behavior.unit.spec.js b/frontend/test/metabase/lib/click-behavior.unit.spec.js
index e2b7889c4c40dfd1737b51648221052a318dcedb..2aed2215e2c53e976d1653c57b78cdfe3478dbdc 100644
--- a/frontend/test/metabase/lib/click-behavior.unit.spec.js
+++ b/frontend/test/metabase/lib/click-behavior.unit.spec.js
@@ -1,11 +1,13 @@
 import _ from "underscore";
 import { metadata, PRODUCTS } from "__support__/sample_database_fixture";
 import * as dateFormatUtils from "metabase/lib/formatting/date";
+import { createMockCard } from "metabase-types/api/mocks";
 import {
   getDataFromClicked,
   getTargetsWithSourceFilters,
   formatSourceForTarget,
 } from "metabase-lib/parameters/utils/click-behavior";
+import Question from "metabase-lib/Question";
 
 describe("metabase/lib/click-behavior", () => {
   describe("getDataFromClicked", () => {
@@ -78,22 +80,25 @@ describe("metabase/lib/click-behavior", () => {
     it("should produce a template tag target", () => {
       const [{ id, name, target }] = getTargetsWithSourceFilters({
         isDash: false,
-        object: {
-          dataset_query: {
-            type: "native",
-            native: {
-              query: "{{foo}}",
-              "template-tags": {
-                my_variable: {
-                  "display-name": "My Variable",
-                  id: "foo123",
-                  name: "my_variable",
-                  type: "text",
+        object: new Question(
+          createMockCard({
+            dataset_query: {
+              type: "native",
+              native: {
+                query: "{{foo}}",
+                "template-tags": {
+                  my_variable: {
+                    "display-name": "My Variable",
+                    id: "foo123",
+                    name: "my_variable",
+                    type: "text",
+                  },
                 },
               },
             },
-          },
-        },
+          }),
+          metadata,
+        ),
         metadata: {},
       });
       expect(id).toEqual("foo123");
@@ -104,25 +109,28 @@ describe("metabase/lib/click-behavior", () => {
     it("should produce a template tag dimension target", () => {
       const [{ id, name, target }] = getTargetsWithSourceFilters({
         isDash: false,
-        object: {
-          dataset_query: {
-            type: "native",
-            native: {
-              query: "{{my_field_filter}}",
-              "template-tags": {
-                my_field_filter: {
-                  default: null,
-                  dimension: ["field", PRODUCTS.CATEGORY.id, null],
-                  "display-name": "My Field Filter",
-                  id: "foo123",
-                  name: "my_field_filter",
-                  type: "dimension",
-                  "widget-type": "category",
+        object: new Question(
+          createMockCard({
+            dataset_query: {
+              type: "native",
+              native: {
+                query: "{{my_field_filter}}",
+                "template-tags": {
+                  my_field_filter: {
+                    default: null,
+                    dimension: ["field", PRODUCTS.CATEGORY.id, null],
+                    "display-name": "My Field Filter",
+                    id: "foo123",
+                    name: "my_field_filter",
+                    type: "dimension",
+                    "widget-type": "category",
+                  },
                 },
               },
             },
-          },
-        },
+          }),
+          metadata,
+        ),
         metadata,
       });
       expect(id).toEqual("foo123");
@@ -279,22 +287,25 @@ describe("metabase/lib/click-behavior", () => {
         it(`should filter sources for a ${targetVariableType} variable target`, () => {
           const [{ sourceFilters }] = getTargetsWithSourceFilters({
             isDash: false,
-            object: {
-              dataset_query: {
-                type: "native",
-                native: {
-                  query: "{{foo}}",
-                  "template-tags": {
-                    my_variable: {
-                      "display-name": "My Variable",
-                      id: "foo123",
-                      name: "my_variable",
-                      type: targetVariableType,
+            object: new Question(
+              createMockCard({
+                dataset_query: {
+                  type: "native",
+                  native: {
+                    query: "{{foo}}",
+                    "template-tags": {
+                      my_variable: {
+                        "display-name": "My Variable",
+                        id: "foo123",
+                        name: "my_variable",
+                        type: targetVariableType,
+                      },
                     },
                   },
                 },
-              },
-            },
+              }),
+              metadata,
+            ),
             metadata,
           });
 
@@ -359,25 +370,28 @@ describe("metabase/lib/click-behavior", () => {
         it(`should filter sources for a ${field.base_type} dimension target`, () => {
           const [{ sourceFilters }] = getTargetsWithSourceFilters({
             isDash: false,
-            object: {
-              dataset_query: {
-                type: "native",
-                native: {
-                  query: "{{my_field_filter}}",
-                  "template-tags": {
-                    my_field_filter: {
-                      default: null,
-                      dimension: ["field", field.id, null],
-                      "display-name": "My Field Filter",
-                      id: "foo123",
-                      name: "my_field_filter",
-                      type: "dimension",
-                      "widget-type": "category",
+            object: new Question(
+              createMockCard({
+                dataset_query: {
+                  type: "native",
+                  native: {
+                    query: "{{my_field_filter}}",
+                    "template-tags": {
+                      my_field_filter: {
+                        default: null,
+                        dimension: ["field", field.id, null],
+                        "display-name": "My Field Filter",
+                        id: "foo123",
+                        name: "my_field_filter",
+                        type: "dimension",
+                        "widget-type": "category",
+                      },
                     },
                   },
                 },
-              },
-            },
+              }),
+              metadata,
+            ),
             metadata,
           });
           const filteredSources = _.mapObject(sources, (sources, sourceType) =>