diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 348dc054f576a983a31995aeb859d59d3a5bc93f..20d1dce3905bd8bf59cf9b459c5a536ae9a20e89 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -50,6 +50,9 @@ export default {
     },
   },
   computed: {
+    issuableId() {
+      return getIdFromGraphQLId(this.issuable.id);
+    },
     createdInPastDay() {
       const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
       return createdSecondsAgo < SECONDS_IN_DAY;
@@ -61,7 +64,7 @@ export default {
       return this.issuable.gitlabWebUrl || this.issuable.webUrl;
     },
     authorId() {
-      return getIdFromGraphQLId(`${this.author.id}`);
+      return getIdFromGraphQLId(this.author.id);
     },
     isIssuableUrlExternal() {
       return isExternal(this.webUrl);
@@ -70,10 +73,10 @@ export default {
       return this.issuable.labels?.nodes || this.issuable.labels || [];
     },
     labelIdsString() {
-      return JSON.stringify(this.labels.map((label) => label.id));
+      return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id)));
     },
     assignees() {
-      return this.issuable.assignees || [];
+      return this.issuable.assignees?.nodes || this.issuable.assignees || [];
     },
     createdAt() {
       return sprintf(__('created %{timeAgo}'), {
@@ -157,7 +160,7 @@ export default {
 
 <template>
   <li
-    :id="`issuable_${issuable.id}`"
+    :id="`issuable_${issuableId}`"
     class="issue gl-px-5!"
     :class="{ closed: issuable.closedAt, today: createdInPastDay }"
     :data-labels="labelIdsString"
@@ -167,7 +170,7 @@ export default {
         <gl-form-checkbox
           class="gl-mr-0"
           :checked="checked"
-          :data-id="issuable.id"
+          :data-id="issuableId"
           @input="$emit('checked-input', $event)"
         >
           <span class="gl-sr-only">{{ issuable.title }}</span>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index 45584205be0237281cdd11681565b1b8678d2205..a19c76cfe3f6d0f45309f9ed31e9caa049b6e4fc 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -1,7 +1,7 @@
 <script>
-import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
 import { uniqueId } from 'lodash';
-
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
 import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
 
@@ -19,6 +19,7 @@ export default {
     tag: 'ul',
   },
   components: {
+    GlKeysetPagination,
     GlSkeletonLoading,
     IssuableTabs,
     FilteredSearchBar,
@@ -140,6 +141,21 @@ export default {
       required: false,
       default: false,
     },
+    useKeysetPagination: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    hasNextPage: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    hasPreviousPage: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   data() {
     return {
@@ -211,7 +227,7 @@ export default {
   },
   methods: {
     issuableId(issuable) {
-      return issuable.id || issuable.iid || uniqueId();
+      return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
     },
     issuableChecked(issuable) {
       return this.checkedIssuables[this.issuableId(issuable)]?.checked;
@@ -315,8 +331,16 @@ export default {
         <slot v-else name="empty-state"></slot>
       </template>
 
+      <div v-if="showPaginationControls && useKeysetPagination" class="gl-text-center gl-mt-3">
+        <gl-keyset-pagination
+          :has-next-page="hasNextPage"
+          :has-previous-page="hasPreviousPage"
+          @next="$emit('next-page')"
+          @prev="$emit('previous-page')"
+        />
+      </div>
       <gl-pagination
-        v-if="showPaginationControls"
+        v-else-if="showPaginationControls"
         :per-page="defaultPageSize"
         :total-items="totalItems"
         :value="currentPage"
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 8d00d337baca333a57c0a816f19d80095f6defe8..70d73aca9257d3416a67f216f45e7502bb916613 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -42,6 +42,9 @@ export default {
       }
       return __('Milestone');
     },
+    milestoneLink() {
+      return this.issue.milestone.webPath || this.issue.milestone.webUrl;
+    },
     dueDate() {
       return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
     },
@@ -49,7 +52,7 @@ export default {
       return isInPast(new Date(this.issue.dueDate));
     },
     timeEstimate() {
-      return this.issue.timeStats?.humanTimeEstimate;
+      return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
     },
     showHealthStatus() {
       return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
@@ -85,7 +88,7 @@ export default {
       class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
       data-testid="issuable-milestone"
     >
-      <gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
+      <gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate">
         <gl-icon name="clock" />
         {{ issue.milestone.title }}
       </gl-link>
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index d5cab77f26c6dc0f0211b94fa10bc5bd4123b230..dbf7717b248ad8cf63fe84768e65b7d162292266 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -9,7 +9,7 @@ import {
   GlTooltipDirective,
 } from '@gitlab/ui';
 import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { toNumber } from 'lodash';
+import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
 import createFlash from '~/flash';
 import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
 import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -17,13 +17,12 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
 import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
 import {
   API_PARAM,
-  apiSortParams,
   CREATED_DESC,
   i18n,
+  initialPageParams,
   MAX_LIST_SIZE,
   PAGE_SIZE,
   PARAM_DUE_DATE,
-  PARAM_PAGE,
   PARAM_SORT,
   PARAM_STATE,
   RELATIVE_POSITION_DESC,
@@ -49,7 +48,8 @@ import {
   getSortOptions,
 } from '~/issues_list/utils';
 import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
 import {
   DEFAULT_NONE_ANY,
   OPERATOR_IS_ONLY,
@@ -107,9 +107,6 @@ export default {
     emptyStateSvgPath: {
       default: '',
     },
-    endpoint: {
-      default: '',
-    },
     exportCsvPath: {
       default: '',
     },
@@ -173,15 +170,43 @@ export default {
       dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
       exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
       filterTokens: getFilterTokens(window.location.search),
-      isLoading: false,
       issues: [],
-      page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
+      pageInfo: {},
+      pageParams: initialPageParams,
       showBulkEditSidebar: false,
       sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
       state: state || IssuableStates.Opened,
       totalIssues: 0,
     };
   },
+  apollo: {
+    issues: {
+      query: getIssuesQuery,
+      variables() {
+        return {
+          projectPath: this.projectPath,
+          search: this.searchQuery,
+          sort: this.sortKey,
+          state: this.state,
+          ...this.pageParams,
+          ...this.apiFilterParams,
+        };
+      },
+      update: ({ project }) => project.issues.nodes,
+      result({ data }) {
+        this.pageInfo = data.project.issues.pageInfo;
+        this.totalIssues = data.project.issues.count;
+        this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
+      },
+      error(error) {
+        createFlash({ message: this.$options.i18n.errorFetchingIssues, captureError: true, error });
+      },
+      skip() {
+        return !this.hasProjectIssues;
+      },
+      debounce: 200,
+    },
+  },
   computed: {
     hasSearch() {
       return this.searchQuery || Object.keys(this.urlFilterParams).length;
@@ -348,7 +373,6 @@ export default {
 
       return {
         due_date: this.dueDateFilter,
-        page: this.page,
         search: this.searchQuery,
         state: this.state,
         ...urlSortParams[this.sortKey],
@@ -361,7 +385,6 @@ export default {
   },
   mounted() {
     eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
-    this.fetchIssues();
   },
   beforeDestroy() {
     eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
@@ -406,54 +429,11 @@ export default {
     fetchUsers(search) {
       return axios.get(this.autocompleteUsersPath, { params: { search } });
     },
-    fetchIssues() {
-      if (!this.hasProjectIssues) {
-        return undefined;
-      }
-
-      this.isLoading = true;
-
-      const filterParams = {
-        ...this.apiFilterParams,
-      };
-
-      if (filterParams.epic_id) {
-        filterParams.epic_id = filterParams.epic_id.split('::&').pop();
-      } else if (filterParams['not[epic_id]']) {
-        filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
-      }
-
-      return axios
-        .get(this.endpoint, {
-          params: {
-            due_date: this.dueDateFilter,
-            page: this.page,
-            per_page: PAGE_SIZE,
-            search: this.searchQuery,
-            state: this.state,
-            with_labels_details: true,
-            ...apiSortParams[this.sortKey],
-            ...filterParams,
-          },
-        })
-        .then(({ data, headers }) => {
-          this.page = Number(headers['x-page']);
-          this.totalIssues = Number(headers['x-total']);
-          this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
-          this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
-        })
-        .catch(() => {
-          createFlash({ message: this.$options.i18n.errorFetchingIssues });
-        })
-        .finally(() => {
-          this.isLoading = false;
-        });
-    },
     getExportCsvPathWithQuery() {
       return `${this.exportCsvPath}${window.location.search}`;
     },
     getStatus(issue) {
-      if (issue.closedAt && issue.movedToId) {
+      if (issue.closedAt && issue.moved) {
         return this.$options.i18n.closedMoved;
       }
       if (issue.closedAt) {
@@ -484,18 +464,26 @@ export default {
     },
     handleClickTab(state) {
       if (this.state !== state) {
-        this.page = 1;
+        this.pageParams = initialPageParams;
       }
       this.state = state;
-      this.fetchIssues();
     },
     handleFilter(filter) {
       this.filterTokens = filter;
-      this.fetchIssues();
     },
-    handlePageChange(page) {
-      this.page = page;
-      this.fetchIssues();
+    handleNextPage() {
+      this.pageParams = {
+        afterCursor: this.pageInfo.endCursor,
+        firstPageSize: PAGE_SIZE,
+      };
+      scrollUp();
+    },
+    handlePreviousPage() {
+      this.pageParams = {
+        beforeCursor: this.pageInfo.startCursor,
+        lastPageSize: PAGE_SIZE,
+      };
+      scrollUp();
     },
     handleReorder({ newIndex, oldIndex }) {
       const issueToMove = this.issues[oldIndex];
@@ -530,9 +518,11 @@ export default {
           createFlash({ message: this.$options.i18n.reorderError });
         });
     },
-    handleSort(value) {
-      this.sortKey = value;
-      this.fetchIssues();
+    handleSort(sortKey) {
+      if (this.sortKey !== sortKey) {
+        this.pageParams = initialPageParams;
+      }
+      this.sortKey = sortKey;
     },
     toggleBulkEditSidebar(showBulkEditSidebar) {
       this.showBulkEditSidebar = showBulkEditSidebar;
@@ -556,18 +546,18 @@ export default {
       :tabs="$options.IssuableListTabs"
       :current-tab="state"
       :tab-counts="tabCounts"
-      :issuables-loading="isLoading"
+      :issuables-loading="$apollo.queries.issues.loading"
       :is-manual-ordering="isManualOrdering"
       :show-bulk-edit-sidebar="showBulkEditSidebar"
       :show-pagination-controls="showPaginationControls"
-      :total-items="totalIssues"
-      :current-page="page"
-      :previous-page="page - 1"
-      :next-page="page + 1"
+      :use-keyset-pagination="true"
+      :has-next-page="pageInfo.hasNextPage"
+      :has-previous-page="pageInfo.hasPreviousPage"
       :url-params="urlParams"
       @click-tab="handleClickTab"
       @filter="handleFilter"
-      @page-change="handlePageChange"
+      @next-page="handleNextPage"
+      @previous-page="handlePreviousPage"
       @reorder="handleReorder"
       @sort="handleSort"
       @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
@@ -646,7 +636,7 @@ export default {
         </li>
         <blocking-issues-count
           class="gl-display-none gl-sm-display-block"
-          :blocking-issues-count="issuable.blockingIssuesCount"
+          :blocking-issues-count="issuable.blockedByCount"
           :is-list-item="true"
         />
       </template>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 06e140d642037c3db4fc3ea202030a42ec54c349..76006f9081dc3d33d93b011808a2bf413ec1af5d 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -101,10 +101,13 @@ export const i18n = {
 export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
 
 export const PARAM_DUE_DATE = 'due_date';
-export const PARAM_PAGE = 'page';
 export const PARAM_SORT = 'sort';
 export const PARAM_STATE = 'state';
 
+export const initialPageParams = {
+  firstPageSize: PAGE_SIZE,
+};
+
 export const DUE_DATE_NONE = '0';
 export const DUE_DATE_ANY = '';
 export const DUE_DATE_OVERDUE = 'overdue';
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index d0c9462a3d75677e6850990af8d3bb0f965497bb..97b9a9a115de92f8d0561bd8500af224f7c44439 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -73,6 +73,13 @@ export function mountIssuesListApp() {
     return false;
   }
 
+  Vue.use(VueApollo);
+
+  const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+  const apolloProvider = new VueApollo({
+    defaultClient,
+  });
+
   const {
     autocompleteAwardEmojisPath,
     autocompleteUsersPath,
@@ -83,7 +90,6 @@ export function mountIssuesListApp() {
     email,
     emailsHelpPagePath,
     emptyStateSvgPath,
-    endpoint,
     exportCsvPath,
     groupEpicsPath,
     hasBlockedIssuesFeature,
@@ -113,16 +119,13 @@ export function mountIssuesListApp() {
 
   return new Vue({
     el,
-    // Currently does not use Vue Apollo, but need to provide {} for now until the
-    // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
-    apolloProvider: {},
+    apolloProvider,
     provide: {
       autocompleteAwardEmojisPath,
       autocompleteUsersPath,
       calendarPath,
       canBulkUpdate: parseBoolean(canBulkUpdate),
       emptyStateSvgPath,
-      endpoint,
       groupEpicsPath,
       hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
       hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..afd53084ca04e1f34c2f56812df6507db5468acf
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -0,0 +1,45 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "./issue.fragment.graphql"
+
+query getProjectIssues(
+  $projectPath: ID!
+  $search: String
+  $sort: IssueSort
+  $state: IssuableState
+  $assigneeId: String
+  $assigneeUsernames: [String!]
+  $authorUsername: String
+  $labelName: [String]
+  $milestoneTitle: [String]
+  $not: NegatedIssueFilterInput
+  $beforeCursor: String
+  $afterCursor: String
+  $firstPageSize: Int
+  $lastPageSize: Int
+) {
+  project(fullPath: $projectPath) {
+    issues(
+      search: $search
+      sort: $sort
+      state: $state
+      assigneeId: $assigneeId
+      assigneeUsernames: $assigneeUsernames
+      authorUsername: $authorUsername
+      labelName: $labelName
+      milestoneTitle: $milestoneTitle
+      not: $not
+      before: $beforeCursor
+      after: $afterCursor
+      first: $firstPageSize
+      last: $lastPageSize
+    ) {
+      count
+      pageInfo {
+        ...PageInfo
+      }
+      nodes {
+        ...IssueFragment
+      }
+    }
+  }
+}
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..de30d8b4bf669ca369b2dc711a16f9133ec6398d
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -0,0 +1,51 @@
+fragment IssueFragment on Issue {
+  id
+  iid
+  closedAt
+  confidential
+  createdAt
+  downvotes
+  dueDate
+  humanTimeEstimate
+  moved
+  title
+  updatedAt
+  upvotes
+  userDiscussionsCount
+  webUrl
+  assignees {
+    nodes {
+      id
+      avatarUrl
+      name
+      username
+      webUrl
+    }
+  }
+  author {
+    id
+    avatarUrl
+    name
+    username
+    webUrl
+  }
+  labels {
+    nodes {
+      id
+      color
+      title
+      description
+    }
+  }
+  milestone {
+    id
+    dueDate
+    startDate
+    webPath
+    title
+  }
+  taskCompletionStatus {
+    completedCount
+    count
+  }
+}
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 91920277c509ae9823500c3f6589b60f04ca758c..7690773354fe12c490a073a1d615a77ddce43c5d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -190,7 +190,6 @@ def issues_list_data(project, current_user, finder)
       email: current_user&.notification_email,
       emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
       empty_state_svg_path: image_path('illustrations/issues.svg'),
-      endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
       export_csv_path: export_csv_project_issues_path(project),
       has_project_issues: project_issues(project).exists?.to_s,
       import_csv_issues_path: import_csv_namespace_project_issues_path,
diff --git a/ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..d61fbd1c75ff72a88e2f653c47de059ee2528e1f
--- /dev/null
+++ b/ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -0,0 +1,56 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "~/issues_list/queries/issue.fragment.graphql"
+
+query getProjectIssues(
+  $projectPath: ID!
+  $search: String
+  $sort: IssueSort
+  $state: IssuableState
+  $assigneeId: String
+  $assigneeUsernames: [String!]
+  $authorUsername: String
+  $labelName: [String]
+  $milestoneTitle: [String]
+  $epicId: String
+  $iterationId: [ID]
+  $iterationWildcardId: IterationWildcardId
+  $weight: String
+  $not: NegatedIssueFilterInput
+  $beforeCursor: String
+  $afterCursor: String
+  $firstPageSize: Int
+  $lastPageSize: Int
+) {
+  project(fullPath: $projectPath) {
+    issues(
+      search: $search
+      sort: $sort
+      state: $state
+      assigneeId: $assigneeId
+      assigneeUsernames: $assigneeUsernames
+      authorUsername: $authorUsername
+      labelName: $labelName
+      milestoneTitle: $milestoneTitle
+      epicId: $epicId
+      iterationId: $iterationId
+      iterationWildcardId: $iterationWildcardId
+      weight: $weight
+      not: $not
+      before: $beforeCursor
+      after: $afterCursor
+      first: $firstPageSize
+      last: $lastPageSize
+    ) {
+      count
+      pageInfo {
+        ...PageInfo
+      }
+      nodes {
+        ...IssueFragment
+        blockedByCount
+        healthStatus
+        weight
+      }
+    }
+  }
+}
diff --git a/ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap b/ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap
index ff91d7ec30d0889e90357f11e100282c5d340a35..8ef1102e3368832cc5232b5b14521dd4249a5590 100644
--- a/ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap
+++ b/ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap
@@ -6,6 +6,8 @@ Object {
   "currentTab": "opened",
   "defaultPageSize": 2,
   "enableLabelPermalinks": true,
+  "hasNextPage": false,
+  "hasPreviousPage": false,
   "initialFilterValue": Array [
     Object {
       "type": "filtered-search-term",
@@ -75,5 +77,6 @@ Object {
     "sort": "created_desc",
     "state": "opened",
   },
+  "useKeysetPagination": false,
 }
 `;
diff --git a/spec/frontend/issuable_list/components/issuable_list_root_spec.js b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
index 38d6d6d86bc035d4edd2dd9746d9672a862b483b..7dddd2c3405bc8581b4a91f7cfa68f35085abc23 100644
--- a/spec/frontend/issuable_list/components/issuable_list_root_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import VueDraggable from 'vuedraggable';
 
@@ -11,9 +11,12 @@ import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filte
 
 import { mockIssuableListProps, mockIssuables } from '../mock_data';
 
-const createComponent = ({ props = mockIssuableListProps, data = {} } = {}) =>
+const createComponent = ({ props = {}, data = {} } = {}) =>
   shallowMount(IssuableListRoot, {
-    propsData: props,
+    propsData: {
+      ...mockIssuableListProps,
+      ...props,
+    },
     data() {
       return data;
     },
@@ -34,6 +37,7 @@ describe('IssuableListRoot', () => {
   let wrapper;
 
   const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
+  const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
   const findGlPagination = () => wrapper.findComponent(GlPagination);
   const findIssuableItem = () => wrapper.findComponent(IssuableItem);
   const findIssuableTabs = () => wrapper.findComponent(IssuableTabs);
@@ -189,15 +193,15 @@ describe('IssuableListRoot', () => {
   });
 
   describe('template', () => {
-    beforeEach(() => {
+    it('renders component container element with class "issuable-list-container"', () => {
       wrapper = createComponent();
-    });
 
-    it('renders component container element with class "issuable-list-container"', () => {
       expect(wrapper.classes()).toContain('issuable-list-container');
     });
 
     it('renders issuable-tabs component', () => {
+      wrapper = createComponent();
+
       const tabsEl = findIssuableTabs();
 
       expect(tabsEl.exists()).toBe(true);
@@ -209,6 +213,8 @@ describe('IssuableListRoot', () => {
     });
 
     it('renders contents for slot "nav-actions" within issuable-tab component', () => {
+      wrapper = createComponent();
+
       const buttonEl = findIssuableTabs().find('button.js-new-issuable');
 
       expect(buttonEl.exists()).toBe(true);
@@ -216,6 +222,8 @@ describe('IssuableListRoot', () => {
     });
 
     it('renders filtered-search-bar component', () => {
+      wrapper = createComponent();
+
       const searchEl = findFilteredSearchBar();
       const {
         namespace,
@@ -239,12 +247,8 @@ describe('IssuableListRoot', () => {
       });
     });
 
-    it('renders gl-loading-icon when `issuablesLoading` prop is true', async () => {
-      wrapper.setProps({
-        issuablesLoading: true,
-      });
-
-      await wrapper.vm.$nextTick();
+    it('renders gl-loading-icon when `issuablesLoading` prop is true', () => {
+      wrapper = createComponent({ props: { issuablesLoading: true } });
 
       expect(wrapper.findAllComponents(GlSkeletonLoading)).toHaveLength(
         wrapper.vm.skeletonItemCount,
@@ -252,6 +256,8 @@ describe('IssuableListRoot', () => {
     });
 
     it('renders issuable-item component for each item within `issuables` array', () => {
+      wrapper = createComponent();
+
       const itemsEl = wrapper.findAllComponents(IssuableItem);
       const mockIssuable = mockIssuableListProps.issuables[0];
 
@@ -262,28 +268,23 @@ describe('IssuableListRoot', () => {
       });
     });
 
-    it('renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty', async () => {
-      wrapper.setProps({
-        issuables: [],
-      });
-
-      await wrapper.vm.$nextTick();
+    it('renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty', () => {
+      wrapper = createComponent({ props: { issuables: [] } });
 
       expect(wrapper.find('p.js-issuable-empty-state').exists()).toBe(true);
       expect(wrapper.find('p.js-issuable-empty-state').text()).toBe('Issuable empty state');
     });
 
-    it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
-      wrapper.setProps({
-        showPaginationControls: true,
-        totalItems: 10,
+    it('renders only gl-pagination when `showPaginationControls` prop is true', () => {
+      wrapper = createComponent({
+        props: {
+          showPaginationControls: true,
+          totalItems: 10,
+        },
       });
 
-      await wrapper.vm.$nextTick();
-
-      const paginationEl = findGlPagination();
-      expect(paginationEl.exists()).toBe(true);
-      expect(paginationEl.props()).toMatchObject({
+      expect(findGlKeysetPagination().exists()).toBe(false);
+      expect(findGlPagination().props()).toMatchObject({
         perPage: 20,
         value: 1,
         prevPage: 0,
@@ -292,32 +293,47 @@ describe('IssuableListRoot', () => {
         align: 'center',
       });
     });
-  });
 
-  describe('events', () => {
-    beforeEach(() => {
+    it('renders only gl-keyset-pagination when `showPaginationControls` and `useKeysetPagination` props are true', () => {
       wrapper = createComponent({
-        data: {
-          checkedIssuables: {
-            [mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
-          },
+        props: {
+          hasNextPage: true,
+          hasPreviousPage: true,
+          showPaginationControls: true,
+          useKeysetPagination: true,
         },
       });
+
+      expect(findGlPagination().exists()).toBe(false);
+      expect(findGlKeysetPagination().props()).toMatchObject({
+        hasNextPage: true,
+        hasPreviousPage: true,
+      });
     });
+  });
+
+  describe('events', () => {
+    const data = {
+      checkedIssuables: {
+        [mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
+      },
+    };
 
     it('issuable-tabs component emits `click-tab` event on `click-tab` event', () => {
+      wrapper = createComponent({ data });
+
       findIssuableTabs().vm.$emit('click');
 
       expect(wrapper.emitted('click-tab')).toBeTruthy();
     });
 
-    it('sets all issuables as checked when filtered-search-bar component emits `checked-input` event', async () => {
+    it('sets all issuables as checked when filtered-search-bar component emits `checked-input` event', () => {
+      wrapper = createComponent({ data });
+
       const searchEl = findFilteredSearchBar();
 
       searchEl.vm.$emit('checked-input', true);
 
-      await wrapper.vm.$nextTick();
-
       expect(searchEl.emitted('checked-input')).toBeTruthy();
       expect(searchEl.emitted('checked-input').length).toBe(1);
 
@@ -328,6 +344,8 @@ describe('IssuableListRoot', () => {
     });
 
     it('filtered-search-bar component emits `filter` event on `onFilter` & `sort` event on `onSort` events', () => {
+      wrapper = createComponent({ data });
+
       const searchEl = findFilteredSearchBar();
 
       searchEl.vm.$emit('onFilter');
@@ -336,13 +354,13 @@ describe('IssuableListRoot', () => {
       expect(wrapper.emitted('sort')).toBeTruthy();
     });
 
-    it('sets an issuable as checked when issuable-item component emits `checked-input` event', async () => {
+    it('sets an issuable as checked when issuable-item component emits `checked-input` event', () => {
+      wrapper = createComponent({ data });
+
       const issuableItem = wrapper.findAllComponents(IssuableItem).at(0);
 
       issuableItem.vm.$emit('checked-input', true);
 
-      await wrapper.vm.$nextTick();
-
       expect(issuableItem.emitted('checked-input')).toBeTruthy();
       expect(issuableItem.emitted('checked-input').length).toBe(1);
 
@@ -353,27 +371,45 @@ describe('IssuableListRoot', () => {
     });
 
     it('emits `update-legacy-bulk-edit` when filtered-search-bar checkbox is checked', () => {
+      wrapper = createComponent({ data });
+
       findFilteredSearchBar().vm.$emit('checked-input');
 
       expect(wrapper.emitted('update-legacy-bulk-edit')).toEqual([[]]);
     });
 
     it('emits `update-legacy-bulk-edit` when issuable-item checkbox is checked', () => {
+      wrapper = createComponent({ data });
+
       findIssuableItem().vm.$emit('checked-input');
 
       expect(wrapper.emitted('update-legacy-bulk-edit')).toEqual([[]]);
     });
 
-    it('gl-pagination component emits `page-change` event on `input` event', async () => {
-      wrapper.setProps({
-        showPaginationControls: true,
-      });
-
-      await wrapper.vm.$nextTick();
+    it('gl-pagination component emits `page-change` event on `input` event', () => {
+      wrapper = createComponent({ data, props: { showPaginationControls: true } });
 
       findGlPagination().vm.$emit('input');
       expect(wrapper.emitted('page-change')).toBeTruthy();
     });
+
+    it.each`
+      event              | glKeysetPaginationEvent
+      ${'next-page'}     | ${'next'}
+      ${'previous-page'} | ${'prev'}
+    `(
+      'emits `$event` event when gl-keyset-pagination emits `$glKeysetPaginationEvent` event',
+      ({ event, glKeysetPaginationEvent }) => {
+        wrapper = createComponent({
+          data,
+          props: { showPaginationControls: true, useKeysetPagination: true },
+        });
+
+        findGlKeysetPagination().vm.$emit(glKeysetPaginationEvent);
+
+        expect(wrapper.emitted(event)).toEqual([[]]);
+      },
+    );
   });
 
   describe('manual sorting', () => {
diff --git a/spec/frontend/issues_list/components/issue_card_time_info_spec.js b/spec/frontend/issues_list/components/issue_card_time_info_spec.js
index 614ad586ec9bd1eacc5839a7de35212733b95c0d..634687e77abc58cef252a31a1bcfbabde01ed98d 100644
--- a/spec/frontend/issues_list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues_list/components/issue_card_time_info_spec.js
@@ -13,12 +13,10 @@ describe('IssuesListApp component', () => {
       dueDate: '2020-12-17',
       startDate: '2020-12-10',
       title: 'My milestone',
-      webUrl: '/milestone/webUrl',
+      webPath: '/milestone/webPath',
     },
     dueDate: '2020-12-12',
-    timeStats: {
-      humanTimeEstimate: '1w',
-    },
+    humanTimeEstimate: '1w',
   };
 
   const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
@@ -56,7 +54,7 @@ describe('IssuesListApp component', () => {
 
       expect(milestone.text()).toBe(issue.milestone.title);
       expect(milestone.find(GlIcon).props('name')).toBe('clock');
-      expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webUrl);
+      expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
     });
 
     describe.each`
@@ -102,7 +100,7 @@ describe('IssuesListApp component', () => {
 
     const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
 
-    expect(timeEstimate.text()).toBe(issue.timeStats.humanTimeEstimate);
+    expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
     expect(timeEstimate.attributes('title')).toBe('Estimate');
     expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
   });
diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js
index d78a436c618b9bc86132f5a36d1d35cb887be346..a3ac57ee1bbb934421e53a2eb89517ea38eba5e3 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -1,9 +1,19 @@
 import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
 import AxiosMockAdapter from 'axios-mock-adapter';
+import { cloneDeep } from 'lodash';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
 import { TEST_HOST } from 'helpers/test_constants';
 import waitForPromises from 'helpers/wait_for_promises';
-import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
+import {
+  getIssuesQueryResponse,
+  filteredTokens,
+  locationSearch,
+  urlParams,
+} from 'jest/issues_list/mock_data';
 import createFlash from '~/flash';
 import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
 import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -14,10 +24,7 @@ import {
   apiSortParams,
   CREATED_DESC,
   DUE_DATE_OVERDUE,
-  PAGE_SIZE,
-  PAGE_SIZE_MANUAL,
   PARAM_DUE_DATE,
-  RELATIVE_POSITION_DESC,
   TOKEN_TYPE_ASSIGNEE,
   TOKEN_TYPE_AUTHOR,
   TOKEN_TYPE_CONFIDENTIAL,
@@ -32,20 +39,26 @@ import {
 import eventHub from '~/issues_list/eventhub';
 import { getSortOptions } from '~/issues_list/utils';
 import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
 import { setUrlParams } from '~/lib/utils/url_utility';
 
 jest.mock('~/flash');
+jest.mock('~/lib/utils/scroll_utils', () => ({
+  scrollUp: jest.fn().mockName('scrollUpMock'),
+}));
 
 describe('IssuesListApp component', () => {
   let axiosMock;
   let wrapper;
 
+  const localVue = createLocalVue();
+  localVue.use(VueApollo);
+
   const defaultProvide = {
     autocompleteUsersPath: 'autocomplete/users/path',
     calendarPath: 'calendar/path',
     canBulkUpdate: false,
     emptyStateSvgPath: 'empty-state.svg',
-    endpoint: 'api/endpoint',
     exportCsvPath: 'export/csv/path',
     hasBlockedIssuesFeature: true,
     hasIssueWeightsFeature: true,
@@ -61,21 +74,13 @@ describe('IssuesListApp component', () => {
     signInPath: 'sign/in/path',
   };
 
-  const state = 'opened';
-  const xPage = 1;
-  const xTotal = 25;
-  const tabCounts = {
-    opened: xTotal,
-    closed: undefined,
-    all: undefined,
-  };
-  const fetchIssuesResponse = {
-    data: [],
-    headers: {
-      'x-page': xPage,
-      'x-total': xTotal,
-    },
-  };
+  let defaultQueryResponse = getIssuesQueryResponse;
+  if (IS_EE) {
+    defaultQueryResponse = cloneDeep(getIssuesQueryResponse);
+    defaultQueryResponse.data.project.issues.nodes[0].blockedByCount = 1;
+    defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null;
+    defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
+  }
 
   const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
   const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
@@ -86,19 +91,26 @@ describe('IssuesListApp component', () => {
   const findGlLink = () => wrapper.findComponent(GlLink);
   const findIssuableList = () => wrapper.findComponent(IssuableList);
 
-  const mountComponent = ({ provide = {}, mountFn = shallowMount } = {}) =>
-    mountFn(IssuesListApp, {
+  const mountComponent = ({
+    provide = {},
+    response = defaultQueryResponse,
+    mountFn = shallowMount,
+  } = {}) => {
+    const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
+    const apolloProvider = createMockApollo(requestHandlers);
+
+    return mountFn(IssuesListApp, {
+      localVue,
+      apolloProvider,
       provide: {
         ...defaultProvide,
         ...provide,
       },
     });
+  };
 
   beforeEach(() => {
     axiosMock = new AxiosMockAdapter(axios);
-    axiosMock
-      .onGet(defaultProvide.endpoint)
-      .reply(200, fetchIssuesResponse.data, fetchIssuesResponse.headers);
   });
 
   afterEach(() => {
@@ -108,28 +120,37 @@ describe('IssuesListApp component', () => {
   });
 
   describe('IssuableList', () => {
-    beforeEach(async () => {
+    beforeEach(() => {
       wrapper = mountComponent();
-      await waitForPromises();
+      jest.runOnlyPendingTimers();
     });
 
     it('renders', () => {
       expect(findIssuableList().props()).toMatchObject({
         namespace: defaultProvide.projectPath,
         recentSearchesStorageKey: 'issues',
-        searchInputPlaceholder: 'Search or filter results…',
+        searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
         sortOptions: getSortOptions(true, true),
         initialSortBy: CREATED_DESC,
+        issuables: getIssuesQueryResponse.data.project.issues.nodes,
         tabs: IssuableListTabs,
         currentTab: IssuableStates.Opened,
-        tabCounts,
-        showPaginationControls: false,
-        issuables: [],
-        totalItems: xTotal,
-        currentPage: xPage,
-        previousPage: xPage - 1,
-        nextPage: xPage + 1,
-        urlParams: { page: xPage, state },
+        tabCounts: {
+          opened: 1,
+          closed: undefined,
+          all: undefined,
+        },
+        issuablesLoading: false,
+        isManualOrdering: false,
+        showBulkEditSidebar: false,
+        showPaginationControls: true,
+        useKeysetPagination: true,
+        hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
+        hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
+        urlParams: {
+          state: IssuableStates.Opened,
+          ...urlSortParams[CREATED_DESC],
+        },
       });
     });
   });
@@ -157,9 +178,9 @@ describe('IssuesListApp component', () => {
 
     describe('csv import/export component', () => {
       describe('when user is signed in', () => {
-        it('renders', async () => {
-          const search = '?page=1&search=refactor&state=opened&sort=created_date';
+        const search = '?search=refactor&state=opened&sort=created_date';
 
+        beforeEach(() => {
           global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
 
           wrapper = mountComponent({
@@ -167,11 +188,13 @@ describe('IssuesListApp component', () => {
             mountFn: mount,
           });
 
-          await waitForPromises();
+          jest.runOnlyPendingTimers();
+        });
 
+        it('renders', () => {
           expect(findCsvImportExportButtons().props()).toMatchObject({
             exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
-            issuableCount: xTotal,
+            issuableCount: 1,
           });
         });
       });
@@ -238,18 +261,6 @@ describe('IssuesListApp component', () => {
       });
     });
 
-    describe('page', () => {
-      it('is set from the url params', () => {
-        const page = 5;
-
-        global.jsdom.reconfigure({ url: setUrlParams({ page }, TEST_HOST) });
-
-        wrapper = mountComponent();
-
-        expect(findIssuableList().props('currentPage')).toBe(page);
-      });
-    });
-
     describe('search', () => {
       it('is set from the url params', () => {
         global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
@@ -326,12 +337,10 @@ describe('IssuesListApp component', () => {
   describe('empty states', () => {
     describe('when there are issues', () => {
       describe('when search returns no results', () => {
-        beforeEach(async () => {
+        beforeEach(() => {
           global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
 
           wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
-
-          await waitForPromises();
         });
 
         it('shows empty state', () => {
@@ -344,10 +353,8 @@ describe('IssuesListApp component', () => {
       });
 
       describe('when "Open" tab has no issues', () => {
-        beforeEach(async () => {
+        beforeEach(() => {
           wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
-
-          await waitForPromises();
         });
 
         it('shows empty state', () => {
@@ -360,14 +367,12 @@ describe('IssuesListApp component', () => {
       });
 
       describe('when "Closed" tab has no issues', () => {
-        beforeEach(async () => {
+        beforeEach(() => {
           global.jsdom.reconfigure({
             url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
           });
 
           wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
-
-          await waitForPromises();
         });
 
         it('shows empty state', () => {
@@ -555,98 +560,70 @@ describe('IssuesListApp component', () => {
   describe('events', () => {
     describe('when "click-tab" event is emitted by IssuableList', () => {
       beforeEach(() => {
-        axiosMock.onGet(defaultProvide.endpoint).reply(200, fetchIssuesResponse.data, {
-          'x-page': 2,
-          'x-total': xTotal,
-        });
-
         wrapper = mountComponent();
 
         findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
       });
 
-      it('makes API call to filter the list by the new state and resets the page to 1', () => {
-        expect(axiosMock.history.get[1].params).toMatchObject({
-          page: 1,
-          state: IssuableStates.Closed,
-        });
+      it('updates to the new tab', () => {
+        expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
       });
     });
 
-    describe('when "page-change" event is emitted by IssuableList', () => {
-      const data = [{ id: 10, title: 'title', state }];
-      const page = 2;
-      const totalItems = 21;
-
-      beforeEach(async () => {
-        axiosMock.onGet(defaultProvide.endpoint).reply(200, data, {
-          'x-page': page,
-          'x-total': totalItems,
-        });
-
-        wrapper = mountComponent();
-
-        findIssuableList().vm.$emit('page-change', page);
-
-        await waitForPromises();
-      });
+    describe.each(['next-page', 'previous-page'])(
+      'when "%s" event is emitted by IssuableList',
+      (event) => {
+        beforeEach(() => {
+          wrapper = mountComponent();
 
-      it('fetches issues with expected params', () => {
-        expect(axiosMock.history.get[1].params).toMatchObject({
-          page,
-          per_page: PAGE_SIZE,
-          state,
-          with_labels_details: true,
+          findIssuableList().vm.$emit(event);
         });
-      });
 
-      it('updates IssuableList with response data', () => {
-        expect(findIssuableList().props()).toMatchObject({
-          issuables: data,
-          totalItems,
-          currentPage: page,
-          previousPage: page - 1,
-          nextPage: page + 1,
-          urlParams: { page, state },
+        it('scrolls to the top', () => {
+          expect(scrollUp).toHaveBeenCalled();
         });
-      });
-    });
+      },
+    );
 
     describe('when "reorder" event is emitted by IssuableList', () => {
-      const issueOne = { id: 1, iid: 101, title: 'Issue one' };
-      const issueTwo = { id: 2, iid: 102, title: 'Issue two' };
-      const issueThree = { id: 3, iid: 103, title: 'Issue three' };
-      const issueFour = { id: 4, iid: 104, title: 'Issue four' };
-      const issues = [issueOne, issueTwo, issueThree, issueFour];
-
-      beforeEach(async () => {
-        axiosMock.onGet(defaultProvide.endpoint).reply(200, issues, fetchIssuesResponse.headers);
-        wrapper = mountComponent();
-        await waitForPromises();
-      });
-
-      describe('when successful', () => {
-        describe.each`
-          description                       | issueToMove   | oldIndex | newIndex | moveBeforeId    | moveAfterId
-          ${'to the beginning of the list'} | ${issueThree} | ${2}     | ${0}     | ${null}         | ${issueOne.id}
-          ${'down the list'}                | ${issueOne}   | ${0}     | ${1}     | ${issueTwo.id}  | ${issueThree.id}
-          ${'up the list'}                  | ${issueThree} | ${2}     | ${1}     | ${issueOne.id}  | ${issueTwo.id}
-          ${'to the end of the list'}       | ${issueTwo}   | ${1}     | ${3}     | ${issueFour.id} | ${null}
-        `(
-          'when moving issue $description',
-          ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
-            it('makes API call to reorder the issue', async () => {
-              findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
-
-              await waitForPromises();
-
-              expect(axiosMock.history.put[0]).toMatchObject({
-                url: `${defaultProvide.issuesPath}/${issueToMove.iid}/reorder`,
-                data: JSON.stringify({ move_before_id: moveBeforeId, move_after_id: moveAfterId }),
-              });
-            });
+      const issueOne = {
+        ...defaultQueryResponse.data.project.issues.nodes[0],
+        id: 'gid://gitlab/Issue/1',
+        iid: 101,
+        title: 'Issue one',
+      };
+      const issueTwo = {
+        ...defaultQueryResponse.data.project.issues.nodes[0],
+        id: 'gid://gitlab/Issue/2',
+        iid: 102,
+        title: 'Issue two',
+      };
+      const issueThree = {
+        ...defaultQueryResponse.data.project.issues.nodes[0],
+        id: 'gid://gitlab/Issue/3',
+        iid: 103,
+        title: 'Issue three',
+      };
+      const issueFour = {
+        ...defaultQueryResponse.data.project.issues.nodes[0],
+        id: 'gid://gitlab/Issue/4',
+        iid: 104,
+        title: 'Issue four',
+      };
+      const response = {
+        data: {
+          project: {
+            issues: {
+              ...defaultQueryResponse.data.project.issues,
+              nodes: [issueOne, issueTwo, issueThree, issueFour],
+            },
           },
-        );
+        },
+      };
+
+      beforeEach(() => {
+        wrapper = mountComponent({ response });
+        jest.runOnlyPendingTimers();
       });
 
       describe('when unsuccessful', () => {
@@ -664,21 +641,16 @@ describe('IssuesListApp component', () => {
 
     describe('when "sort" event is emitted by IssuableList', () => {
       it.each(Object.keys(apiSortParams))(
-        'fetches issues with correct params with payload `%s`',
+        'updates to the new sort when payload is `%s`',
         async (sortKey) => {
           wrapper = mountComponent();
 
           findIssuableList().vm.$emit('sort', sortKey);
 
-          await waitForPromises();
+          jest.runOnlyPendingTimers();
+          await nextTick();
 
-          expect(axiosMock.history.get[1].params).toEqual({
-            page: xPage,
-            per_page: sortKey === RELATIVE_POSITION_DESC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
-            state,
-            with_labels_details: true,
-            ...apiSortParams[sortKey],
-          });
+          expect(findIssuableList().props('urlParams')).toMatchObject(urlSortParams[sortKey]);
         },
       );
     });
@@ -687,13 +659,11 @@ describe('IssuesListApp component', () => {
       beforeEach(() => {
         wrapper = mountComponent();
         jest.spyOn(eventHub, '$emit');
-      });
 
-      it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
         findIssuableList().vm.$emit('update-legacy-bulk-edit');
+      });
 
-        await waitForPromises();
-
+      it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => {
         expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
       });
     });
@@ -705,10 +675,6 @@ describe('IssuesListApp component', () => {
         findIssuableList().vm.$emit('filter', filteredTokens);
       });
 
-      it('makes an API call to search for issues with the search term', () => {
-        expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
-      });
-
       it('updates IssuableList with url params', () => {
         expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
       });
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index 99267fb6e3102429784d1762326b47f63fdaf536..6c669e02070bac8b2ce1710aebf1b341af134eab 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -3,6 +3,73 @@ import {
   OPERATOR_IS_NOT,
 } from '~/vue_shared/components/filtered_search_bar/constants';
 
+export const getIssuesQueryResponse = {
+  data: {
+    project: {
+      issues: {
+        count: 1,
+        pageInfo: {
+          hasNextPage: false,
+          hasPreviousPage: false,
+          startCursor: 'startcursor',
+          endCursor: 'endcursor',
+        },
+        nodes: [
+          {
+            id: 'gid://gitlab/Issue/123456',
+            iid: '789',
+            closedAt: null,
+            confidential: false,
+            createdAt: '2021-05-22T04:08:01Z',
+            downvotes: 2,
+            dueDate: '2021-05-29',
+            humanTimeEstimate: null,
+            moved: false,
+            title: 'Issue title',
+            updatedAt: '2021-05-22T04:08:01Z',
+            upvotes: 3,
+            userDiscussionsCount: 4,
+            webUrl: 'project/-/issues/789',
+            assignees: {
+              nodes: [
+                {
+                  id: 'gid://gitlab/User/234',
+                  avatarUrl: 'avatar/url',
+                  name: 'Marge Simpson',
+                  username: 'msimpson',
+                  webUrl: 'url/msimpson',
+                },
+              ],
+            },
+            author: {
+              id: 'gid://gitlab/User/456',
+              avatarUrl: 'avatar/url',
+              name: 'Homer Simpson',
+              username: 'hsimpson',
+              webUrl: 'url/hsimpson',
+            },
+            labels: {
+              nodes: [
+                {
+                  id: 'gid://gitlab/ProjectLabel/456',
+                  color: '#333',
+                  title: 'Label title',
+                  description: 'Label description',
+                },
+              ],
+            },
+            milestone: null,
+            taskCompletionStatus: {
+              completedCount: 1,
+              count: 2,
+            },
+          },
+        ],
+      },
+    },
+  },
+};
+
 export const locationSearch = [
   '?search=find+issues',
   'author_username=homer',
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 59b42dfca2094273355e807ac638e4946e6ebc7c..a8a227c8ec4e953248b4cbf7758ee5bdc4289113 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -302,7 +302,6 @@
         email: current_user&.notification_email,
         emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
         empty_state_svg_path: '#',
-        endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
         export_csv_path: export_csv_project_issues_path(project),
         has_project_issues: project_issues(project).exists?.to_s,
         import_csv_issues_path: '#',