diff --git a/frontend/src/metabase-types/api/index.ts b/frontend/src/metabase-types/api/index.ts
index 01cb581aa9697d52456be4d1bf539c19b73bdf55..a163fb3d4f208c7b334621d96a03c9d336f5c75a 100644
--- a/frontend/src/metabase-types/api/index.ts
+++ b/frontend/src/metabase-types/api/index.ts
@@ -31,6 +31,7 @@ export * from "./slack";
 export * from "./snippets";
 export * from "./store";
 export * from "./table";
+export * from "./task";
 export * from "./timeline";
 export * from "./user";
 export * from "./util";
diff --git a/frontend/src/metabase-types/api/task.ts b/frontend/src/metabase-types/api/task.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81f6b0d2be5aa9bc8ebdf222a801b38809af4b2a
--- /dev/null
+++ b/frontend/src/metabase-types/api/task.ts
@@ -0,0 +1,15 @@
+import type { DatabaseId } from "./database";
+import type { PaginationRequest, PaginationResponse } from "./pagination";
+
+export interface Task {
+  id: number;
+  db_id: DatabaseId | null;
+  duration: number;
+  started_at: string;
+  ended_at: string;
+  task: string;
+  task_details: Record<string, unknown> | null;
+}
+export type ListTasksRequest = PaginationRequest;
+
+export type ListTasksResponse = { data: Task[] } & PaginationResponse;
diff --git a/frontend/src/metabase/admin/routes.jsx b/frontend/src/metabase/admin/routes.jsx
index 78fcb70a2d14731be2992d3df4ba6828de509c10..045fe50f88bba654c50043d841c6a8b0a5e273dc 100644
--- a/frontend/src/metabase/admin/routes.jsx
+++ b/frontend/src/metabase/admin/routes.jsx
@@ -34,8 +34,8 @@ import {
   ModelCacheRefreshJobs,
   ModelCacheRefreshJobModal,
 } from "metabase/admin/tasks/containers/ModelCacheRefreshJobs";
-import TaskModal from "metabase/admin/tasks/containers/TaskModal";
-import TasksApp from "metabase/admin/tasks/containers/TasksApp";
+import { TaskModal } from "metabase/admin/tasks/containers/TaskModal";
+import { TasksApp } from "metabase/admin/tasks/containers/TasksApp";
 import TroubleshootingApp from "metabase/admin/tasks/containers/TroubleshootingApp";
 import Tools from "metabase/admin/tools/containers/Tools";
 import {
diff --git a/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx b/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx
deleted file mode 100644
index b2d9eda599b6d389697b5d5aca078dc4b0a52381..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/admin/tasks/containers/TaskModal.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/* eslint-disable react/prop-types */
-import { Component } from "react";
-import { connect } from "react-redux";
-import { goBack } from "react-router-redux";
-import { t } from "ttag";
-import _ from "underscore";
-
-import Code from "metabase/components/Code";
-import ModalContent from "metabase/components/ModalContent";
-import Task from "metabase/entities/tasks";
-
-class TaskModalInner extends Component {
-  render() {
-    const { object } = this.props;
-    return (
-      <ModalContent title={t`Task details`} onClose={() => this.props.goBack()}>
-        <Code>{JSON.stringify(object.task_details)}</Code>
-      </ModalContent>
-    );
-  }
-}
-
-const TaskModal = _.compose(
-  Task.load({
-    id: (state, props) => props.params.taskId,
-  }),
-  connect(null, { goBack }),
-)(TaskModalInner);
-
-export default TaskModal;
diff --git a/frontend/src/metabase/admin/tasks/containers/TaskModal.tsx b/frontend/src/metabase/admin/tasks/containers/TaskModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..17482fdda3aca1ea4696a72da60a6661e534aa7f
--- /dev/null
+++ b/frontend/src/metabase/admin/tasks/containers/TaskModal.tsx
@@ -0,0 +1,31 @@
+import { goBack } from "react-router-redux";
+import { t } from "ttag";
+
+import { useGetTaskQuery } from "metabase/api";
+import Code from "metabase/components/Code";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper";
+import ModalContent from "metabase/components/ModalContent";
+import { useDispatch } from "metabase/lib/redux";
+
+type TaskModalProps = {
+  params: { taskId: number };
+};
+
+export const TaskModal = ({ params }: TaskModalProps) => {
+  const dispatch = useDispatch();
+  const { data, isLoading, error } = useGetTaskQuery(params.taskId);
+
+  if (isLoading || error) {
+    return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
+  }
+
+  if (!data) {
+    return null;
+  }
+
+  return (
+    <ModalContent title={t`Task details`} onClose={() => dispatch(goBack())}>
+      <Code>{JSON.stringify(data.task_details)}</Code>
+    </ModalContent>
+  );
+};
diff --git a/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx b/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx
deleted file mode 100644
index 3f1b9911fee5586a1e4b74619472b0cfb4558265..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/* eslint-disable react/prop-types */
-import cx from "classnames";
-import { Component } from "react";
-import { t } from "ttag";
-import _ from "underscore";
-
-import AdminHeader from "metabase/components/AdminHeader";
-import PaginationControls from "metabase/components/PaginationControls";
-import Link from "metabase/core/components/Link";
-import Tooltip from "metabase/core/components/Tooltip";
-import AdminS from "metabase/css/admin.module.css";
-import CS from "metabase/css/core/index.css";
-import Database from "metabase/entities/databases";
-import Task from "metabase/entities/tasks";
-
-import {
-  InfoIcon,
-  SectionControls,
-  SectionHeader,
-  SectionRoot,
-  SectionTitle,
-} from "./TasksApp.styled";
-
-// Please preserve the following 2 @ calls in this order.
-// Otherwise @Database.loadList overrides pagination props
-// that come from @Task.LoadList
-
-class TasksAppInner extends Component {
-  render() {
-    const {
-      tasks,
-      databases,
-      page,
-      pageSize,
-      onNextPage,
-      onPreviousPage,
-      children,
-    } = this.props;
-    const databaseByID = {};
-    // index databases by id for lookup
-    for (const db of databases) {
-      databaseByID[db.id] = db;
-    }
-    return (
-      <SectionRoot>
-        <SectionHeader>
-          <SectionTitle>
-            <AdminHeader title={t`Troubleshooting logs`} />
-            <Tooltip
-              tooltip={t`Trying to get to the bottom of something? This section shows logs of Metabase's background tasks, which can help shed light on what's going on.`}
-            >
-              <InfoIcon name="info" />
-            </Tooltip>
-          </SectionTitle>
-          <SectionControls>
-            <PaginationControls
-              onPreviousPage={onPreviousPage}
-              onNextPage={onNextPage}
-              page={page}
-              pageSize={pageSize}
-              itemsLength={tasks.length}
-            />
-          </SectionControls>
-        </SectionHeader>
-
-        <table className={cx(AdminS.ContentTable, CS.mt2)}>
-          <thead>
-            <tr>
-              <th>{t`Task`}</th>
-              <th>{t`DB Name`}</th>
-              <th>{t`DB Engine`}</th>
-              <th>{t`Started at`}</th>
-              <th>{t`Ended at`}</th>
-              <th>{t`Duration (ms)`}</th>
-              <th>{t`Details`}</th>
-            </tr>
-          </thead>
-          <tbody>
-            {tasks.map(task => {
-              const db = task.db_id ? databaseByID[task.db_id] : null;
-              const name = db ? db.name : null;
-              const engine = db ? db.engine : null;
-              // only want unknown if there is a db on the task and we don't have info
-              return (
-                <tr key={task.id}>
-                  <td className={CS.textBold}>{task.task}</td>
-                  <td>{task.db_id ? name || t`Unknown name` : null}</td>
-                  <td>{task.db_id ? engine || t`Unknown engine` : null}</td>
-                  <td>{task.started_at}</td>
-                  <td>{task.ended_at}</td>
-                  <td>{task.duration}</td>
-                  <td>
-                    <Link
-                      className={cx(CS.link, CS.textBold)}
-                      to={`/admin/troubleshooting/tasks/${task.id}`}
-                    >{t`View`}</Link>
-                  </td>
-                </tr>
-              );
-            })}
-          </tbody>
-        </table>
-        {
-          // render 'children' so that the invididual task modals show up
-          children
-        }
-      </SectionRoot>
-    );
-  }
-}
-
-const TasksApp = _.compose(
-  Database.loadList(),
-  Task.loadList({
-    pageSize: 50,
-  }),
-)(TasksAppInner);
-
-export default TasksApp;
diff --git a/frontend/src/metabase/admin/tasks/containers/TasksApp.tsx b/frontend/src/metabase/admin/tasks/containers/TasksApp.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3e0bd501249aa40478ddd4424b8fb5f722d63395
--- /dev/null
+++ b/frontend/src/metabase/admin/tasks/containers/TasksApp.tsx
@@ -0,0 +1,133 @@
+import cx from "classnames";
+import type { ReactNode } from "react";
+import { useState } from "react";
+import { t } from "ttag";
+import _ from "underscore";
+
+import { useListTasksQuery, useListDatabasesQuery } from "metabase/api";
+import AdminHeader from "metabase/components/AdminHeader";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper/LoadingAndErrorWrapper";
+import PaginationControls from "metabase/components/PaginationControls";
+import Link from "metabase/core/components/Link";
+import AdminS from "metabase/css/admin.module.css";
+import CS from "metabase/css/core/index.css";
+import { Tooltip } from "metabase/ui";
+import type { Database, Task } from "metabase-types/api";
+
+import {
+  InfoIcon,
+  SectionControls,
+  SectionHeader,
+  SectionRoot,
+  SectionTitle,
+} from "./TasksApp.styled";
+
+type TasksAppProps = {
+  children: ReactNode;
+};
+
+export const TasksApp = ({ children }: TasksAppProps) => {
+  const [page, setPage] = useState(0);
+  const pageSize = 50;
+
+  const {
+    data: tasksData,
+    isFetching: isLoadingTasks,
+    error: tasksError,
+  } = useListTasksQuery({
+    limit: pageSize,
+    offset: page * pageSize,
+  });
+
+  const {
+    data: databasesData,
+    isFetching: isLoadingDatabases,
+    error: databasesError,
+  } = useListDatabasesQuery();
+
+  const tasks = tasksData?.data;
+  const databases = databasesData?.data;
+
+  if (isLoadingTasks || isLoadingDatabases || tasksError || databasesError) {
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoadingTasks || isLoadingDatabases}
+        error={tasksError || databasesError}
+      />
+    );
+  }
+
+  if (!tasks || !databases) {
+    return null;
+  }
+
+  // index databases by id for lookup
+  const databaseByID: Record<number, Database> = _.indexBy(databases, "id");
+
+  return (
+    <SectionRoot>
+      <SectionHeader>
+        <SectionTitle>
+          <AdminHeader title={t`Troubleshooting logs`} />
+          <Tooltip
+            label={t`Trying to get to the bottom of something? This section shows logs of Metabase's background tasks, which can help shed light on what's going on.`}
+          >
+            <InfoIcon name="info" />
+          </Tooltip>
+        </SectionTitle>
+        <SectionControls>
+          <PaginationControls
+            onPreviousPage={() => setPage(page - 1)}
+            onNextPage={() => setPage(page + 1)}
+            page={page}
+            pageSize={50}
+            itemsLength={tasks.length}
+            total={tasksData.total}
+          />
+        </SectionControls>
+      </SectionHeader>
+
+      <table className={cx(AdminS.ContentTable, CS.mt2)}>
+        <thead>
+          <tr>
+            <th>{t`Task`}</th>
+            <th>{t`DB Name`}</th>
+            <th>{t`DB Engine`}</th>
+            <th>{t`Started at`}</th>
+            <th>{t`Ended at`}</th>
+            <th>{t`Duration (ms)`}</th>
+            <th>{t`Details`}</th>
+          </tr>
+        </thead>
+        <tbody>
+          {tasks.map((task: Task) => {
+            const db = task.db_id ? databaseByID[task.db_id] : null;
+            const name = db ? db.name : null;
+            const engine = db ? db.engine : null;
+            // only want unknown if there is a db on the task and we don't have info
+            return (
+              <tr key={task.id}>
+                <td className={CS.textBold}>{task.task}</td>
+                <td>{task.db_id ? name || t`Unknown name` : null}</td>
+                <td>{task.db_id ? engine || t`Unknown engine` : null}</td>
+                <td>{task.started_at}</td>
+                <td>{task.ended_at}</td>
+                <td>{task.duration}</td>
+                <td>
+                  <Link
+                    className={cx(CS.link, CS.textBold)}
+                    to={`/admin/troubleshooting/tasks/${task.id}`}
+                  >{t`View`}</Link>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+      {
+        // render 'children' so that the invididual task modals show up
+        children
+      }
+    </SectionRoot>
+  );
+};
diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts
index 3afdb03479716b584ca23da737b3472a28c7bfad..c9933accc5612022ab89549c71c97dd5127c6b71 100644
--- a/frontend/src/metabase/api/index.ts
+++ b/frontend/src/metabase/api/index.ts
@@ -16,6 +16,7 @@ export * from "./search";
 export * from "./segment";
 export * from "./session";
 export * from "./table";
+export * from "./task";
 export * from "./timeline";
 export * from "./timeline-event";
 export * from "./user";
diff --git a/frontend/src/metabase/api/tags/constants.ts b/frontend/src/metabase/api/tags/constants.ts
index 5eaf8c7d9a258719b9ee5b22b8b2d883a4a2cff5..700ded618785b1bd280fbf6912f0a0aa0ac3692e 100644
--- a/frontend/src/metabase/api/tags/constants.ts
+++ b/frontend/src/metabase/api/tags/constants.ts
@@ -17,6 +17,7 @@ export const TAG_TYPES = [
   "snippet",
   "segment",
   "table",
+  "task",
   "timeline",
   "timeline-event",
   "user",
diff --git a/frontend/src/metabase/api/tags/utils.ts b/frontend/src/metabase/api/tags/utils.ts
index 4dfd787865980d0d9cf923f523e0e1f42cb99d8c..379c3e20a58801a7d21c033859531ed022377f4b 100644
--- a/frontend/src/metabase/api/tags/utils.ts
+++ b/frontend/src/metabase/api/tags/utils.ts
@@ -23,6 +23,7 @@ import type {
   SearchResult,
   Segment,
   Table,
+  Task,
   Timeline,
   TimelineEvent,
   UserInfo,
@@ -317,6 +318,14 @@ export function provideTableTags(table: Table): TagDescription<TagType>[] {
   ];
 }
 
+export function provideTaskListTags(tasks: Task[]): TagDescription<TagType>[] {
+  return [listTag("task"), ...tasks.flatMap(provideTaskTags)];
+}
+
+export function provideTaskTags(task: Task): TagDescription<TagType>[] {
+  return [idTag("task", task.id)];
+}
+
 export function provideTimelineEventListTags(
   events: TimelineEvent[],
 ): TagDescription<TagType>[] {
diff --git a/frontend/src/metabase/api/task.ts b/frontend/src/metabase/api/task.ts
new file mode 100644
index 0000000000000000000000000000000000000000..41e9bbb61848232c0eb4e30cc8a2d414e675adf1
--- /dev/null
+++ b/frontend/src/metabase/api/task.ts
@@ -0,0 +1,38 @@
+import type {
+  ListTasksRequest,
+  ListTasksResponse,
+  Task,
+} from "metabase-types/api";
+
+import { Api } from "./api";
+import { provideTaskTags, provideTaskListTags } from "./tags";
+
+export const taskApi = Api.injectEndpoints({
+  endpoints: builder => ({
+    listTasks: builder.query<ListTasksResponse, ListTasksRequest | void>({
+      query: params => ({
+        method: "GET",
+        url: "/api/task",
+        params,
+      }),
+      providesTags: response =>
+        response ? provideTaskListTags(response.data) : [],
+    }),
+    getTask: builder.query<Task, number>({
+      query: id => ({
+        method: "GET",
+        url: `/api/task/${id}`,
+      }),
+      providesTags: task => (task ? provideTaskTags(task) : []),
+    }),
+    getTasksInfo: builder.query<unknown, void>({
+      query: () => ({
+        method: "GET",
+        url: "/api/task/info",
+      }),
+    }),
+  }),
+});
+
+export const { useListTasksQuery, useGetTaskQuery, useGetTasksInfoQuery } =
+  taskApi;
diff --git a/frontend/src/metabase/components/PaginationControls/PaginationControls.jsx b/frontend/src/metabase/components/PaginationControls/PaginationControls.jsx
index 507de8b69c2bc3adb4cbb713df9285243c729309..91ac1054c453d183a8e6585a1eb485d4e85c9a1f 100644
--- a/frontend/src/metabase/components/PaginationControls/PaginationControls.jsx
+++ b/frontend/src/metabase/components/PaginationControls/PaginationControls.jsx
@@ -21,6 +21,9 @@ export default function PaginationControls({
     return null;
   }
 
+  const isLastPage = (pageIndex, pageSize, total) =>
+    pageIndex === Math.ceil(total / pageSize) - 1;
+
   const isPreviousDisabled = page === 0;
   const isNextDisabled =
     total != null ? isLastPage(page, pageSize, total) : !onNextPage;
@@ -73,6 +76,3 @@ PaginationControls.propTypes = {
 PaginationControls.defaultProps = {
   showTotal: false,
 };
-
-export const isLastPage = (pageIndex, pageSize, total) =>
-  pageIndex === Math.ceil(total / pageSize) - 1;
diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js
index 0d6e1dd3fef6f615ed052f8e8746fc0304d94402..d60172da2f83d4d9c5f1ea51bd72fd16f7be9df3 100644
--- a/frontend/src/metabase/entities/index.js
+++ b/frontend/src/metabase/entities/index.js
@@ -20,7 +20,6 @@ export { default as tables } from "./tables";
 export { default as fields } from "./fields";
 export { default as metrics } from "./metrics";
 export { default as segments } from "./segments";
-export { default as tasks } from "./tasks";
 
 export { default as users } from "./users";
 export { default as groups } from "./groups";
diff --git a/frontend/src/metabase/entities/tasks.js b/frontend/src/metabase/entities/tasks.js
deleted file mode 100644
index 4fac2c88c6b818ba6ed2aa0610093164590c6c05..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/entities/tasks.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createEntity } from "metabase/lib/entities";
-
-/**
- * @deprecated use "metabase/api" instead
- */
-export default createEntity({
-  name: "tasks",
-  path: "/api/task",
-});
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index f4a72317a48e939dbe45e067b28074e825ab6eed..1f75317e95e99623c9a9d43c8b8e3e3472d75c06 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -470,7 +470,6 @@ export const I18NApi = {
 };
 
 export const TaskApi = {
-  get: GET("/api/task"),
   getJobsInfo: GET("/api/task/info"),
 };