diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index e53bfea7389367f2e605c247e755c5228ff21afc..5bc855d0c18eeca39aadcce97f4815a7bdb63110 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,112 +1,265 @@
 <script>
-import { GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { GlButton } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { historyPushState } from '~/lib/utils/common_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
 import { __ } from '~/locale';
+import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
+import { convertAllReleasesGraphQLResponse } from '~/releases/util';
+import allReleasesQuery from '../graphql/queries/all_releases.query.graphql';
 import ReleaseBlock from './release_block.vue';
 import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
+import ReleasesEmptyState from './releases_empty_state.vue';
 import ReleasesPagination from './releases_pagination.vue';
 import ReleasesSort from './releases_sort.vue';
 
 export default {
-  name: 'ReleasesApp',
+  name: 'ReleasesIndexApp',
   components: {
-    GlEmptyState,
-    GlLink,
     GlButton,
     ReleaseBlock,
-    ReleasesPagination,
     ReleaseSkeletonLoader,
+    ReleasesEmptyState,
+    ReleasesPagination,
     ReleasesSort,
   },
+  inject: {
+    projectPath: {
+      default: '',
+    },
+    newReleasePath: {
+      default: '',
+    },
+  },
+  apollo: {
+    /**
+     * The same query as `fullGraphqlResponse`, except that it limits its
+     * results to a single item. This causes this request to complete much more
+     * quickly than `fullGraphqlResponse`, which allows the page to show
+     * meaningful content to the user much earlier.
+     */
+    singleGraphqlResponse: {
+      query: allReleasesQuery,
+      // This trick only works when paginating _forward_.
+      // When paginating backwards, limiting the query to a single item loads
+      // the _last_ item in the page, which is not useful for our purposes.
+      skip() {
+        return !this.includeSingleQuery;
+      },
+      variables() {
+        return {
+          ...this.queryVariables,
+          first: 1,
+        };
+      },
+      update(data) {
+        return { data };
+      },
+      error() {
+        this.singleRequestError = true;
+      },
+    },
+    fullGraphqlResponse: {
+      query: allReleasesQuery,
+      variables() {
+        return this.queryVariables;
+      },
+      update(data) {
+        return { data };
+      },
+      error(error) {
+        this.fullRequestError = true;
+
+        createFlash({
+          message: this.$options.i18n.errorMessage,
+          captureError: true,
+          error,
+        });
+      },
+    },
+  },
+  data() {
+    return {
+      singleRequestError: false,
+      fullRequestError: false,
+      cursors: {
+        before: getParameterByName('before'),
+        after: getParameterByName('after'),
+      },
+      sort: DEFAULT_SORT,
+    };
+  },
   computed: {
-    ...mapState('index', [
-      'documentationPath',
-      'illustrationPath',
-      'newReleasePath',
-      'isLoading',
-      'releases',
-      'hasError',
-    ]),
-    shouldRenderEmptyState() {
-      return !this.releases.length && !this.hasError && !this.isLoading;
+    queryVariables() {
+      let paginationParams = { first: PAGE_SIZE };
+      if (this.cursors.after) {
+        paginationParams = {
+          after: this.cursors.after,
+          first: PAGE_SIZE,
+        };
+      } else if (this.cursors.before) {
+        paginationParams = {
+          before: this.cursors.before,
+          last: PAGE_SIZE,
+        };
+      }
+
+      return {
+        fullPath: this.projectPath,
+        ...paginationParams,
+        sort: this.sort,
+      };
+    },
+    /**
+     * @returns {Boolean} Whether or not to request/include
+     * the results of the single-item query
+     */
+    includeSingleQuery() {
+      return Boolean(!this.cursors.before || this.cursors.after);
+    },
+    isSingleRequestLoading() {
+      return this.$apollo.queries.singleGraphqlResponse.loading;
     },
-    shouldRenderSuccessState() {
-      return this.releases.length && !this.isLoading && !this.hasError;
+    isFullRequestLoading() {
+      return this.$apollo.queries.fullGraphqlResponse.loading;
+    },
+    /**
+     * @returns {Boolean} `true` if the `singleGraphqlResponse`
+     * query has finished loading without errors
+     */
+    isSingleRequestLoaded() {
+      return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project);
+    },
+    /**
+     * @returns {Boolean} `true` if the `fullGraphqlResponse`
+     * query has finished loading without errors
+     */
+    isFullRequestLoaded() {
+      return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project);
+    },
+    releases() {
+      if (this.isFullRequestLoaded) {
+        return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data;
+      }
+
+      if (this.isSingleRequestLoaded && this.includeSingleQuery) {
+        return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data;
+      }
+
+      return [];
     },
-    emptyStateText() {
-      return __(
-        "Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.",
+    pageInfo() {
+      if (!this.isFullRequestLoaded) {
+        return {
+          hasPreviousPage: false,
+          hasNextPage: false,
+        };
+      }
+
+      return this.fullGraphqlResponse.data.project.releases.pageInfo;
+    },
+    shouldRenderEmptyState() {
+      return this.isFullRequestLoaded && this.releases.length === 0;
+    },
+    shouldRenderLoadingIndicator() {
+      return (
+        (this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) ||
+        (this.isFullRequestLoading && !this.fullRequestError)
       );
     },
+    shouldRenderPagination() {
+      return this.isFullRequestLoaded && !this.shouldRenderEmptyState;
+    },
   },
   created() {
-    this.fetchReleases();
+    this.updateQueryParamsFromUrl();
 
-    window.addEventListener('popstate', this.fetchReleases);
+    window.addEventListener('popstate', this.updateQueryParamsFromUrl);
+  },
+  destroyed() {
+    window.removeEventListener('popstate', this.updateQueryParamsFromUrl);
   },
   methods: {
-    ...mapActions('index', {
-      fetchReleasesStoreAction: 'fetchReleases',
-    }),
-    fetchReleases() {
-      this.fetchReleasesStoreAction({
-        before: getParameterByName('before'),
-        after: getParameterByName('after'),
-      });
+    getReleaseKey(release, index) {
+      return [release.tagName, release.name, index].join('|');
+    },
+    updateQueryParamsFromUrl() {
+      this.cursors.before = getParameterByName('before');
+      this.cursors.after = getParameterByName('after');
+    },
+    onPaginationButtonPress() {
+      this.updateQueryParamsFromUrl();
+
+      // In some cases, Apollo Client is able to pull its results from the cache instead of making
+      // a new network request. In these cases, the page's content gets swapped out immediately without
+      // changing the page's scroll, leaving the user looking at the bottom of the new page.
+      // To make the experience consistent, regardless of how the data is sourced, we manually
+      // scroll to the top of the page every time a pagination button is pressed.
+      scrollUp();
+    },
+    onSortChanged(newSort) {
+      if (this.sort === newSort) {
+        return;
+      }
+
+      // Remove the "before" and "after" query parameters from the URL,
+      // effectively placing the user back on page 1 of the results.
+      // This prevents the frontend from requesting the results sorted
+      // by one field (e.g. `released_at`) while using a pagination cursor
+      // intended for a different field (e.g.) `created_at`).
+      // For more details, see the MR that introduced this change:
+      // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63434
+      historyPushState(
+        setUrlParams({
+          before: null,
+          after: null,
+        }),
+      );
+
+      this.updateQueryParamsFromUrl();
+
+      this.sort = newSort;
     },
   },
+  i18n: {
+    newRelease: __('New release'),
+    errorMessage: __('An error occurred while fetching the releases. Please try again.'),
+  },
 };
 </script>
 <template>
   <div class="flex flex-column mt-2">
     <div class="gl-align-self-end gl-mb-3">
-      <releases-sort class="gl-mr-2" @sort:changed="fetchReleases" />
+      <releases-sort :value="sort" class="gl-mr-2" @input="onSortChanged" />
 
       <gl-button
         v-if="newReleasePath"
         :href="newReleasePath"
         :aria-describedby="shouldRenderEmptyState && 'releases-description'"
         category="primary"
-        variant="confirm"
-        data-testid="new-release-button"
+        variant="success"
+        >{{ $options.i18n.newRelease }}</gl-button
       >
-        {{ __('New release') }}
-      </gl-button>
     </div>
 
-    <release-skeleton-loader v-if="isLoading" />
-
-    <gl-empty-state
-      v-else-if="shouldRenderEmptyState"
-      data-testid="empty-state"
-      :title="__('Getting started with releases')"
-      :svg-path="illustrationPath"
-    >
-      <template #description>
-        <span id="releases-description">
-          {{ emptyStateText }}
-          <gl-link
-            :href="documentationPath"
-            :aria-label="__('Releases documentation')"
-            target="_blank"
-          >
-            {{ __('More information') }}
-          </gl-link>
-        </span>
-      </template>
-    </gl-empty-state>
-
-    <div v-else-if="shouldRenderSuccessState" data-testid="success-state">
-      <release-block
-        v-for="(release, index) in releases"
-        :key="index"
-        :release="release"
-        :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
-      />
-    </div>
+    <releases-empty-state v-if="shouldRenderEmptyState" />
+
+    <release-block
+      v-for="(release, index) in releases"
+      :key="getReleaseKey(release, index)"
+      :release="release"
+      :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
+    />
+
+    <release-skeleton-loader v-if="shouldRenderLoadingIndicator" />
 
-    <releases-pagination v-if="!isLoading" />
+    <releases-pagination
+      v-if="shouldRenderPagination"
+      :page-info="pageInfo"
+      @prev="onPaginationButtonPress"
+      @next="onPaginationButtonPress"
+    />
   </div>
 </template>
 <style>
diff --git a/app/assets/javascripts/releases/components/app_index_apollo_client.vue b/app/assets/javascripts/releases/components/app_index_apollo_client.vue
deleted file mode 100644
index f49c44a399f2455b78cb4811cd658dee781e2b18..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/components/app_index_apollo_client.vue
+++ /dev/null
@@ -1,275 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
-import createFlash from '~/flash';
-import { historyPushState } from '~/lib/utils/common_utils';
-import { scrollUp } from '~/lib/utils/scroll_utils';
-import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
-import { convertAllReleasesGraphQLResponse } from '~/releases/util';
-import ReleaseBlock from './release_block.vue';
-import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
-import ReleasesEmptyState from './releases_empty_state.vue';
-import ReleasesPaginationApolloClient from './releases_pagination_apollo_client.vue';
-import ReleasesSortApolloClient from './releases_sort_apollo_client.vue';
-
-export default {
-  name: 'ReleasesIndexApolloClientApp',
-  components: {
-    GlButton,
-    ReleaseBlock,
-    ReleaseSkeletonLoader,
-    ReleasesEmptyState,
-    ReleasesPaginationApolloClient,
-    ReleasesSortApolloClient,
-  },
-  inject: {
-    projectPath: {
-      default: '',
-    },
-    newReleasePath: {
-      default: '',
-    },
-  },
-  apollo: {
-    /**
-     * The same query as `fullGraphqlResponse`, except that it limits its
-     * results to a single item. This causes this request to complete much more
-     * quickly than `fullGraphqlResponse`, which allows the page to show
-     * meaningful content to the user much earlier.
-     */
-    singleGraphqlResponse: {
-      query: allReleasesQuery,
-      // This trick only works when paginating _forward_.
-      // When paginating backwards, limiting the query to a single item loads
-      // the _last_ item in the page, which is not useful for our purposes.
-      skip() {
-        return !this.includeSingleQuery;
-      },
-      variables() {
-        return {
-          ...this.queryVariables,
-          first: 1,
-        };
-      },
-      update(data) {
-        return { data };
-      },
-      error() {
-        this.singleRequestError = true;
-      },
-    },
-    fullGraphqlResponse: {
-      query: allReleasesQuery,
-      variables() {
-        return this.queryVariables;
-      },
-      update(data) {
-        return { data };
-      },
-      error(error) {
-        this.fullRequestError = true;
-
-        createFlash({
-          message: this.$options.i18n.errorMessage,
-          captureError: true,
-          error,
-        });
-      },
-    },
-  },
-  data() {
-    return {
-      singleRequestError: false,
-      fullRequestError: false,
-      cursors: {
-        before: getParameterByName('before'),
-        after: getParameterByName('after'),
-      },
-      sort: DEFAULT_SORT,
-    };
-  },
-  computed: {
-    queryVariables() {
-      let paginationParams = { first: PAGE_SIZE };
-      if (this.cursors.after) {
-        paginationParams = {
-          after: this.cursors.after,
-          first: PAGE_SIZE,
-        };
-      } else if (this.cursors.before) {
-        paginationParams = {
-          before: this.cursors.before,
-          last: PAGE_SIZE,
-        };
-      }
-
-      return {
-        fullPath: this.projectPath,
-        ...paginationParams,
-        sort: this.sort,
-      };
-    },
-    /**
-     * @returns {Boolean} Whether or not to request/include
-     * the results of the single-item query
-     */
-    includeSingleQuery() {
-      return Boolean(!this.cursors.before || this.cursors.after);
-    },
-    isSingleRequestLoading() {
-      return this.$apollo.queries.singleGraphqlResponse.loading;
-    },
-    isFullRequestLoading() {
-      return this.$apollo.queries.fullGraphqlResponse.loading;
-    },
-    /**
-     * @returns {Boolean} `true` if the `singleGraphqlResponse`
-     * query has finished loading without errors
-     */
-    isSingleRequestLoaded() {
-      return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project);
-    },
-    /**
-     * @returns {Boolean} `true` if the `fullGraphqlResponse`
-     * query has finished loading without errors
-     */
-    isFullRequestLoaded() {
-      return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project);
-    },
-    releases() {
-      if (this.isFullRequestLoaded) {
-        return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data;
-      }
-
-      if (this.isSingleRequestLoaded && this.includeSingleQuery) {
-        return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data;
-      }
-
-      return [];
-    },
-    pageInfo() {
-      if (!this.isFullRequestLoaded) {
-        return {
-          hasPreviousPage: false,
-          hasNextPage: false,
-        };
-      }
-
-      return this.fullGraphqlResponse.data.project.releases.pageInfo;
-    },
-    shouldRenderEmptyState() {
-      return this.isFullRequestLoaded && this.releases.length === 0;
-    },
-    shouldRenderLoadingIndicator() {
-      return (
-        (this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) ||
-        (this.isFullRequestLoading && !this.fullRequestError)
-      );
-    },
-    shouldRenderPagination() {
-      return this.isFullRequestLoaded && !this.shouldRenderEmptyState;
-    },
-  },
-  created() {
-    this.updateQueryParamsFromUrl();
-
-    window.addEventListener('popstate', this.updateQueryParamsFromUrl);
-  },
-  destroyed() {
-    window.removeEventListener('popstate', this.updateQueryParamsFromUrl);
-  },
-  methods: {
-    getReleaseKey(release, index) {
-      return [release.tagName, release.name, index].join('|');
-    },
-    updateQueryParamsFromUrl() {
-      this.cursors.before = getParameterByName('before');
-      this.cursors.after = getParameterByName('after');
-    },
-    onPaginationButtonPress() {
-      this.updateQueryParamsFromUrl();
-
-      // In some cases, Apollo Client is able to pull its results from the cache instead of making
-      // a new network request. In these cases, the page's content gets swapped out immediately without
-      // changing the page's scroll, leaving the user looking at the bottom of the new page.
-      // To make the experience consistent, regardless of how the data is sourced, we manually
-      // scroll to the top of the page every time a pagination button is pressed.
-      scrollUp();
-    },
-    onSortChanged(newSort) {
-      if (this.sort === newSort) {
-        return;
-      }
-
-      // Remove the "before" and "after" query parameters from the URL,
-      // effectively placing the user back on page 1 of the results.
-      // This prevents the frontend from requesting the results sorted
-      // by one field (e.g. `released_at`) while using a pagination cursor
-      // intended for a different field (e.g.) `created_at`).
-      // For more details, see the MR that introduced this change:
-      // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63434
-      historyPushState(
-        setUrlParams({
-          before: null,
-          after: null,
-        }),
-      );
-
-      this.updateQueryParamsFromUrl();
-
-      this.sort = newSort;
-    },
-  },
-  i18n: {
-    newRelease: __('New release'),
-    errorMessage: __('An error occurred while fetching the releases. Please try again.'),
-  },
-};
-</script>
-<template>
-  <div class="flex flex-column mt-2">
-    <div class="gl-align-self-end gl-mb-3">
-      <releases-sort-apollo-client :value="sort" class="gl-mr-2" @input="onSortChanged" />
-
-      <gl-button
-        v-if="newReleasePath"
-        :href="newReleasePath"
-        :aria-describedby="shouldRenderEmptyState && 'releases-description'"
-        category="primary"
-        variant="success"
-        >{{ $options.i18n.newRelease }}</gl-button
-      >
-    </div>
-
-    <releases-empty-state v-if="shouldRenderEmptyState" />
-
-    <release-block
-      v-for="(release, index) in releases"
-      :key="getReleaseKey(release, index)"
-      :release="release"
-      :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
-    />
-
-    <release-skeleton-loader v-if="shouldRenderLoadingIndicator" />
-
-    <releases-pagination-apollo-client
-      v-if="shouldRenderPagination"
-      :page-info="pageInfo"
-      @prev="onPaginationButtonPress"
-      @next="onPaginationButtonPress"
-    />
-  </div>
-</template>
-<style>
-.linked-card::after {
-  width: 1px;
-  content: ' ';
-  border: 1px solid #e5e5e5;
-  height: 17px;
-  top: 100%;
-  position: absolute;
-  left: 32px;
-}
-</style>
diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue
index fddf85ead1e1da4b7826bdff71ababe6d437854e..52ad991d61a1847ce03497809270bdfa9d4b6b29 100644
--- a/app/assets/javascripts/releases/components/releases_pagination.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination.vue
@@ -1,26 +1,24 @@
 <script>
 import { GlKeysetPagination } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { isBoolean } from 'lodash';
 import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
 
 export default {
-  name: 'ReleasesPaginationGraphql',
+  name: 'ReleasesPagination',
   components: { GlKeysetPagination },
-  computed: {
-    ...mapState('index', ['pageInfo']),
-    showPagination() {
-      return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
+  props: {
+    pageInfo: {
+      type: Object,
+      required: true,
+      validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage),
     },
   },
   methods: {
-    ...mapActions('index', ['fetchReleases']),
     onPrev(before) {
       historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
-      this.fetchReleases({ before });
     },
     onNext(after) {
       historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
-      this.fetchReleases({ after });
     },
   },
 };
@@ -28,8 +26,10 @@ export default {
 <template>
   <div class="gl-display-flex gl-justify-content-center">
     <gl-keyset-pagination
-      v-if="showPagination"
       v-bind="pageInfo"
+      :prev-text="__('Prev')"
+      :next-text="__('Next')"
+      v-on="$listeners"
       @prev="onPrev($event)"
       @next="onNext($event)"
     />
diff --git a/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue b/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue
deleted file mode 100644
index 73339677a4b22a98729c3e7691eebc030f12ef8d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import { GlKeysetPagination } from '@gitlab/ui';
-import { isBoolean } from 'lodash';
-import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
-
-export default {
-  name: 'ReleasesPaginationApolloClient',
-  components: { GlKeysetPagination },
-  props: {
-    pageInfo: {
-      type: Object,
-      required: true,
-      validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage),
-    },
-  },
-  methods: {
-    onPrev(before) {
-      historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
-    },
-    onNext(after) {
-      historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
-    },
-  },
-};
-</script>
-<template>
-  <div class="gl-display-flex gl-justify-content-center">
-    <gl-keyset-pagination
-      v-bind="pageInfo"
-      :prev-text="__('Prev')"
-      :next-text="__('Next')"
-      v-on="$listeners"
-      @prev="onPrev($event)"
-      @next="onNext($event)"
-    />
-  </div>
-</template>
diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue
index d4210dad19c12b02d4935e77e9ae34a0dcf1dbf0..0f14b579da09a2d3a7e805798cb92e443d8ad330 100644
--- a/app/assets/javascripts/releases/components/releases_sort.vue
+++ b/app/assets/javascripts/releases/components/releases_sort.vue
@@ -1,7 +1,17 @@
 <script>
 import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { ASCENDING_ORDER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants';
+import {
+  ASCENDING_ORDER,
+  DESCENDING_ORDER,
+  SORT_OPTIONS,
+  RELEASED_AT,
+  CREATED_AT,
+  RELEASED_AT_ASC,
+  RELEASED_AT_DESC,
+  CREATED_ASC,
+  ALL_SORTS,
+  SORT_MAP,
+} from '../constants';
 
 export default {
   name: 'ReleasesSort',
@@ -9,35 +19,54 @@ export default {
     GlSorting,
     GlSortingItem,
   },
+  props: {
+    value: {
+      type: String,
+      required: true,
+      validator: (sort) => ALL_SORTS.includes(sort),
+    },
+  },
   computed: {
-    ...mapState('index', {
-      orderBy: (state) => state.sorting.orderBy,
-      sort: (state) => state.sorting.sort,
-    }),
+    orderBy() {
+      if (this.value === RELEASED_AT_ASC || this.value === RELEASED_AT_DESC) {
+        return RELEASED_AT;
+      }
+
+      return CREATED_AT;
+    },
+    direction() {
+      if (this.value === RELEASED_AT_ASC || this.value === CREATED_ASC) {
+        return ASCENDING_ORDER;
+      }
+
+      return DESCENDING_ORDER;
+    },
     sortOptions() {
       return SORT_OPTIONS;
     },
     sortText() {
-      const option = this.sortOptions.find((s) => s.orderBy === this.orderBy);
-      return option.label;
+      return this.sortOptions.find((s) => s.orderBy === this.orderBy).label;
     },
-    isSortAscending() {
-      return this.sort === ASCENDING_ORDER;
+    isDirectionAscending() {
+      return this.direction === ASCENDING_ORDER;
     },
   },
   methods: {
-    ...mapActions('index', ['setSorting']),
     onDirectionChange() {
-      const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
-      this.setSorting({ sort });
-      this.$emit('sort:changed');
+      const direction = this.isDirectionAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
+      this.emitInputEventIfChanged(this.orderBy, direction);
     },
     onSortItemClick(item) {
-      this.setSorting({ orderBy: item });
-      this.$emit('sort:changed');
+      this.emitInputEventIfChanged(item.orderBy, this.direction);
     },
     isActiveSortItem(item) {
-      return this.orderBy === item;
+      return this.orderBy === item.orderBy;
+    },
+    emitInputEventIfChanged(orderBy, direction) {
+      const newSort = SORT_MAP[orderBy][direction];
+      if (newSort !== this.value) {
+        this.$emit('input', SORT_MAP[orderBy][direction]);
+      }
     },
   },
 };
@@ -46,15 +75,15 @@ export default {
 <template>
   <gl-sorting
     :text="sortText"
-    :is-ascending="isSortAscending"
+    :is-ascending="isDirectionAscending"
     data-testid="releases-sort"
     @sortDirectionChange="onDirectionChange"
   >
     <gl-sorting-item
-      v-for="item in sortOptions"
+      v-for="item of sortOptions"
       :key="item.orderBy"
-      :active="isActiveSortItem(item.orderBy)"
-      @click="onSortItemClick(item.orderBy)"
+      :active="isActiveSortItem(item)"
+      @click="onSortItemClick(item)"
     >
       {{ item.label }}
     </gl-sorting-item>
diff --git a/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue b/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue
deleted file mode 100644
index 7257b34bbf6ec3d88b1fd22dc8a558cf9a041d30..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import {
-  ASCENDING_ORDER,
-  DESCENDING_ORDER,
-  SORT_OPTIONS,
-  RELEASED_AT,
-  CREATED_AT,
-  RELEASED_AT_ASC,
-  RELEASED_AT_DESC,
-  CREATED_ASC,
-  ALL_SORTS,
-  SORT_MAP,
-} from '../constants';
-
-export default {
-  name: 'ReleasesSortApolloclient',
-  components: {
-    GlSorting,
-    GlSortingItem,
-  },
-  props: {
-    value: {
-      type: String,
-      required: true,
-      validator: (sort) => ALL_SORTS.includes(sort),
-    },
-  },
-  computed: {
-    orderBy() {
-      if (this.value === RELEASED_AT_ASC || this.value === RELEASED_AT_DESC) {
-        return RELEASED_AT;
-      }
-
-      return CREATED_AT;
-    },
-    direction() {
-      if (this.value === RELEASED_AT_ASC || this.value === CREATED_ASC) {
-        return ASCENDING_ORDER;
-      }
-
-      return DESCENDING_ORDER;
-    },
-    sortOptions() {
-      return SORT_OPTIONS;
-    },
-    sortText() {
-      return this.sortOptions.find((s) => s.orderBy === this.orderBy).label;
-    },
-    isDirectionAscending() {
-      return this.direction === ASCENDING_ORDER;
-    },
-  },
-  methods: {
-    onDirectionChange() {
-      const direction = this.isDirectionAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
-      this.emitInputEventIfChanged(this.orderBy, direction);
-    },
-    onSortItemClick(item) {
-      this.emitInputEventIfChanged(item.orderBy, this.direction);
-    },
-    isActiveSortItem(item) {
-      return this.orderBy === item.orderBy;
-    },
-    emitInputEventIfChanged(orderBy, direction) {
-      const newSort = SORT_MAP[orderBy][direction];
-      if (newSort !== this.value) {
-        this.$emit('input', SORT_MAP[orderBy][direction]);
-      }
-    },
-  },
-};
-</script>
-
-<template>
-  <gl-sorting
-    :text="sortText"
-    :is-ascending="isDirectionAscending"
-    data-testid="releases-sort"
-    @sortDirectionChange="onDirectionChange"
-  >
-    <gl-sorting-item
-      v-for="item of sortOptions"
-      :key="item.orderBy"
-      :active="isActiveSortItem(item)"
-      @click="onSortItemClick(item)"
-    >
-      {{ item.label }}
-    </gl-sorting-item>
-  </gl-sorting>
-</template>
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index 7f67f7d11a3a45e65f7273042e59bc4d549bdf8c..bda7ac52a478f958d243f2757f9c12aee1d1996b 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -1,12 +1,4 @@
-#import "../fragments/release.fragment.graphql"
-
-# This query is identical to
-# `app/graphql/queries/releases/all_releases.query.graphql`.
-# These two queries should be kept in sync.
-# When the `releases_index_apollo_client` feature flag is
-# removed, this query should be removed entirely.
-
-query allReleasesDeprecated(
+query allReleases(
   $fullPath: ID!
   $first: Int
   $last: Int
@@ -20,7 +12,87 @@ query allReleasesDeprecated(
     releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) {
       __typename
       nodes {
-        ...Release
+        __typename
+        name
+        tagName
+        tagPath
+        descriptionHtml
+        releasedAt
+        createdAt
+        upcomingRelease
+        assets {
+          __typename
+          count
+          sources {
+            __typename
+            nodes {
+              __typename
+              format
+              url
+            }
+          }
+          links {
+            __typename
+            nodes {
+              __typename
+              id
+              name
+              url
+              directAssetUrl
+              linkType
+              external
+            }
+          }
+        }
+        evidences {
+          __typename
+          nodes {
+            __typename
+            id
+            filepath
+            collectedAt
+            sha
+          }
+        }
+        links {
+          __typename
+          editUrl
+          selfUrl
+          openedIssuesUrl
+          closedIssuesUrl
+          openedMergeRequestsUrl
+          mergedMergeRequestsUrl
+          closedMergeRequestsUrl
+        }
+        commit {
+          __typename
+          id
+          sha
+          webUrl
+          title
+        }
+        author {
+          __typename
+          id
+          webUrl
+          avatarUrl
+          username
+        }
+        milestones {
+          __typename
+          nodes {
+            __typename
+            id
+            title
+            description
+            webPath
+            stats {
+              __typename
+              totalIssuesCount
+              closedIssuesCount
+            }
+          }
+        }
       }
       pageInfo {
         __typename
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 86fa72d1496a6a95e799bc05a50c17e9c35d02a6..afb8ab461cd82c5096ebeddbd01833ec73f8c713 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -1,50 +1,32 @@
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
-import Vuex from 'vuex';
 import createDefaultClient from '~/lib/graphql';
 import ReleaseIndexApp from './components/app_index.vue';
-import ReleaseIndexApollopClientApp from './components/app_index_apollo_client.vue';
-import createStore from './stores';
-import createIndexModule from './stores/modules/index';
 
 export default () => {
   const el = document.getElementById('js-releases-page');
 
-  if (window.gon?.features?.releasesIndexApolloClient) {
-    Vue.use(VueApollo);
+  Vue.use(VueApollo);
 
-    const apolloProvider = new VueApollo({
-      defaultClient: createDefaultClient(
-        {},
-        {
-          // This page attempts to decrease the perceived loading time
-          // by sending two requests: one request for the first item only (which
-          // completes relatively quickly), and one for all the items (which is slower).
-          // By default, Apollo Client batches these requests together, which defeats
-          // the purpose of making separate requests. So we explicitly
-          // disable batching on this page.
-          batchMax: 1,
-        },
-      ),
-    });
-
-    return new Vue({
-      el,
-      apolloProvider,
-      provide: { ...el.dataset },
-      render: (h) => h(ReleaseIndexApollopClientApp),
-    });
-  }
-
-  Vue.use(Vuex);
+  const apolloProvider = new VueApollo({
+    defaultClient: createDefaultClient(
+      {},
+      {
+        // This page attempts to decrease the perceived loading time
+        // by sending two requests: one request for the first item only (which
+        // completes relatively quickly), and one for all the items (which is slower).
+        // By default, Apollo Client batches these requests together, which defeats
+        // the purpose of making separate requests. So we explicitly
+        // disable batching on this page.
+        batchMax: 1,
+      },
+    ),
+  });
 
   return new Vue({
     el,
-    store: createStore({
-      modules: {
-        index: createIndexModule(el.dataset),
-      },
-    }),
+    apolloProvider,
+    provide: { ...el.dataset },
     render: (h) => h(ReleaseIndexApp),
   });
 };
diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js
deleted file mode 100644
index d3bb11cab30b31ff87cbd85f65f69374b8b0127c..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/stores/modules/index/actions.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import { PAGE_SIZE } from '~/releases/constants';
-import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
-import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-import * as types from './mutation_types';
-
-/**
- * Gets a paginated list of releases from the GraphQL endpoint
- *
- * @param {Object} vuexParams
- * @param {Object} actionParams
- * @param {String} [actionParams.before] A GraphQL cursor. If provided,
- * the items returned will proceed the provided cursor.
- * @param {String} [actionParams.after] A GraphQL cursor. If provided,
- * the items returned will follow the provided cursor.
- */
-export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => {
-  commit(types.REQUEST_RELEASES);
-
-  const { sort, orderBy } = state.sorting;
-  const orderByParam = orderBy === 'created_at' ? 'created' : orderBy;
-  const sortParams = `${orderByParam}_${sort}`.toUpperCase();
-
-  let paginationParams;
-  if (!before && !after) {
-    paginationParams = { first: PAGE_SIZE };
-  } else if (before && !after) {
-    paginationParams = { last: PAGE_SIZE, before };
-  } else if (!before && after) {
-    paginationParams = { first: PAGE_SIZE, after };
-  } else {
-    throw new Error(
-      'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
-    );
-  }
-
-  gqClient
-    .query({
-      query: allReleasesQuery,
-      variables: {
-        fullPath: state.projectPath,
-        sort: sortParams,
-        ...paginationParams,
-      },
-    })
-    .then((response) => {
-      const { data, paginationInfo: pageInfo } = convertAllReleasesGraphQLResponse(response);
-
-      commit(types.RECEIVE_RELEASES_SUCCESS, {
-        data,
-        pageInfo,
-      });
-    })
-    .catch(() => dispatch('receiveReleasesError'));
-};
-
-export const receiveReleasesError = ({ commit }) => {
-  commit(types.RECEIVE_RELEASES_ERROR);
-  createFlash({
-    message: __('An error occurred while fetching the releases. Please try again.'),
-  });
-};
-
-export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
diff --git a/app/assets/javascripts/releases/stores/modules/index/index.js b/app/assets/javascripts/releases/stores/modules/index/index.js
deleted file mode 100644
index d5ca191153a4f27487b63bd138f83eaa63be31d2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/stores/modules/index/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as actions from './actions';
-import mutations from './mutations';
-import createState from './state';
-
-export default (initialState) => ({
-  namespaced: true,
-  actions,
-  mutations,
-  state: createState(initialState),
-});
diff --git a/app/assets/javascripts/releases/stores/modules/index/mutation_types.js b/app/assets/javascripts/releases/stores/modules/index/mutation_types.js
deleted file mode 100644
index 669168efb889ad8d2895a94f31973d8e4274e8a1..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/stores/modules/index/mutation_types.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const REQUEST_RELEASES = 'REQUEST_RELEASES';
-export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS';
-export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR';
-export const SET_SORTING = 'SET_SORTING';
diff --git a/app/assets/javascripts/releases/stores/modules/index/mutations.js b/app/assets/javascripts/releases/stores/modules/index/mutations.js
deleted file mode 100644
index 55a8a488be896aac18fd9be992001f10ba25ee6f..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/stores/modules/index/mutations.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
-  /**
-   * Sets isLoading to true while the request is being made.
-   * @param {Object} state
-   */
-  [types.REQUEST_RELEASES](state) {
-    state.isLoading = true;
-  },
-
-  /**
-   * Sets isLoading to false.
-   * Sets hasError to false.
-   * Sets the received data
-   * Sets the received pagination information
-   * @param {Object} state
-   * @param {Object} resp
-   */
-  [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) {
-    state.hasError = false;
-    state.isLoading = false;
-    state.releases = data;
-    state.pageInfo = pageInfo;
-  },
-
-  /**
-   * Sets isLoading to false.
-   * Sets hasError to true.
-   * Resets the data
-   * @param {Object} state
-   * @param {Object} data
-   */
-  [types.RECEIVE_RELEASES_ERROR](state) {
-    state.isLoading = false;
-    state.releases = [];
-    state.hasError = true;
-    state.pageInfo = {};
-  },
-
-  [types.SET_SORTING](state, sorting) {
-    state.sorting = { ...state.sorting, ...sorting };
-  },
-};
diff --git a/app/assets/javascripts/releases/stores/modules/index/state.js b/app/assets/javascripts/releases/stores/modules/index/state.js
deleted file mode 100644
index 5e1aaab7b58967cf2be51b4110ccb346e6af3e27..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/releases/stores/modules/index/state.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { DESCENDING_ORDER, RELEASED_AT } from '../../../constants';
-
-export default ({
-  projectId,
-  projectPath,
-  documentationPath,
-  illustrationPath,
-  newReleasePath = '',
-}) => ({
-  projectId,
-  projectPath,
-  documentationPath,
-  illustrationPath,
-  newReleasePath,
-
-  isLoading: false,
-  hasError: false,
-  releases: [],
-  pageInfo: {},
-  sorting: {
-    sort: DESCENDING_ORDER,
-    orderBy: RELEASED_AT,
-  },
-});
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 1a2baf96020a44e253125cb70b4a1cf557fa8c65..19413d97d9d1a79dcba56d97295df85c364ad28d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -8,9 +8,6 @@ class Projects::ReleasesController < Projects::ApplicationController
   before_action :authorize_update_release!, only: %i[edit update]
   before_action :authorize_create_release!, only: :new
   before_action :validate_suffix_path, :fetch_latest_tag, only: :latest_permalink
-  before_action only: :index do
-    push_frontend_feature_flag(:releases_index_apollo_client, project, default_enabled: :yaml)
-  end
 
   feature_category :release_orchestration
 
diff --git a/app/graphql/queries/releases/all_releases.query.graphql b/app/graphql/queries/releases/all_releases.query.graphql
deleted file mode 100644
index 150f59832f3d37ab7802c6df1dafbea34f00d57a..0000000000000000000000000000000000000000
--- a/app/graphql/queries/releases/all_releases.query.graphql
+++ /dev/null
@@ -1,109 +0,0 @@
-# This query is identical to
-# `app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql`.
-# These two queries should be kept in sync.
-query allReleases(
-  $fullPath: ID!
-  $first: Int
-  $last: Int
-  $before: String
-  $after: String
-  $sort: ReleaseSort
-) {
-  project(fullPath: $fullPath) {
-    __typename
-    id
-    releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) {
-      __typename
-      nodes {
-        __typename
-        name
-        tagName
-        tagPath
-        descriptionHtml
-        releasedAt
-        createdAt
-        upcomingRelease
-        assets {
-          __typename
-          count
-          sources {
-            __typename
-            nodes {
-              __typename
-              format
-              url
-            }
-          }
-          links {
-            __typename
-            nodes {
-              __typename
-              id
-              name
-              url
-              directAssetUrl
-              linkType
-              external
-            }
-          }
-        }
-        evidences {
-          __typename
-          nodes {
-            __typename
-            id
-            filepath
-            collectedAt
-            sha
-          }
-        }
-        links {
-          __typename
-          editUrl
-          selfUrl
-          openedIssuesUrl
-          closedIssuesUrl
-          openedMergeRequestsUrl
-          mergedMergeRequestsUrl
-          closedMergeRequestsUrl
-        }
-        commit {
-          __typename
-          id
-          sha
-          webUrl
-          title
-        }
-        author {
-          __typename
-          id
-          webUrl
-          avatarUrl
-          username
-        }
-        milestones {
-          __typename
-          nodes {
-            __typename
-            id
-            title
-            description
-            webPath
-            stats {
-              __typename
-              totalIssuesCount
-              closedIssuesCount
-            }
-          }
-        }
-      }
-      pageInfo {
-        __typename
-        startCursor
-        hasPreviousPage
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-}
diff --git a/config/feature_flags/development/releases_index_apollo_client.yml b/config/feature_flags/development/releases_index_apollo_client.yml
deleted file mode 100644
index 072d72af5736b7636ea462adc8dca8d73ad67bb6..0000000000000000000000000000000000000000
--- a/config/feature_flags/development/releases_index_apollo_client.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: releases_index_apollo_client
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61828
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331006
-milestone: '14.0'
-type: development
-group: group::release
-default_enabled: true
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index 98935fdf8729fdebf8085a870607e5cc0e474515..a7348b62fc0061d08a4d982353b2eef05a69a1de 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -24,129 +24,111 @@
     stub_default_url_options(host: 'localhost')
   end
 
-  shared_examples 'releases index page' do
-    context('when the user is a maintainer') do
-      before do
-        sign_in(maintainer)
+  context('when the user is a maintainer') do
+    before do
+      sign_in(maintainer)
 
-        visit project_releases_path(project)
+      visit project_releases_path(project)
 
-        wait_for_requests
-      end
+      wait_for_requests
+    end
 
-      it 'sees the release' do
-        page.within("##{release_v1.tag}") do
-          expect(page).to have_content(release_v1.name)
-          expect(page).to have_content(release_v1.tag)
-          expect(page).not_to have_content('Upcoming Release')
-        end
+    it 'sees the release' do
+      page.within("##{release_v1.tag}") do
+        expect(page).to have_content(release_v1.name)
+        expect(page).to have_content(release_v1.tag)
+        expect(page).not_to have_content('Upcoming Release')
       end
+    end
 
-      it 'renders the correct links', :aggregate_failures do
-        page.within("##{release_v1.tag} .js-assets-list") do
-          external_link_indicator_selector = '[data-testid="external-link-indicator"]'
+    it 'renders the correct links', :aggregate_failures do
+      page.within("##{release_v1.tag} .js-assets-list") do
+        external_link_indicator_selector = '[data-testid="external-link-indicator"]'
 
-          expect(page).to have_link internal_link.name, href: internal_link.url
-          expect(find_link(internal_link.name)).not_to have_css(external_link_indicator_selector)
+        expect(page).to have_link internal_link.name, href: internal_link.url
+        expect(find_link(internal_link.name)).not_to have_css(external_link_indicator_selector)
 
-          expect(page).to have_link internal_link_with_redirect.name, href: Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{internal_link_with_redirect.filepath}"
-          expect(find_link(internal_link_with_redirect.name)).not_to have_css(external_link_indicator_selector)
+        expect(page).to have_link internal_link_with_redirect.name, href: Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{internal_link_with_redirect.filepath}"
+        expect(find_link(internal_link_with_redirect.name)).not_to have_css(external_link_indicator_selector)
 
-          expect(page).to have_link external_link.name, href: external_link.url
-          expect(find_link(external_link.name)).to have_css(external_link_indicator_selector)
-        end
+        expect(page).to have_link external_link.name, href: external_link.url
+        expect(find_link(external_link.name)).to have_css(external_link_indicator_selector)
       end
+    end
 
-      context 'with an upcoming release' do
-        it 'sees the upcoming tag' do
-          page.within("##{release_v3.tag}") do
-            expect(page).to have_content('Upcoming Release')
-          end
+    context 'with an upcoming release' do
+      it 'sees the upcoming tag' do
+        page.within("##{release_v3.tag}") do
+          expect(page).to have_content('Upcoming Release')
         end
       end
+    end
 
-      context 'with a tag containing a slash' do
-        it 'sees the release' do
-          page.within("##{release_v2.tag.parameterize}") do
-            expect(page).to have_content(release_v2.name)
-            expect(page).to have_content(release_v2.tag)
-          end
+    context 'with a tag containing a slash' do
+      it 'sees the release' do
+        page.within("##{release_v2.tag.parameterize}") do
+          expect(page).to have_content(release_v2.name)
+          expect(page).to have_content(release_v2.tag)
         end
       end
+    end
 
-      context 'sorting' do
-        def sort_page(by:, direction:)
-          within '[data-testid="releases-sort"]' do
-            find('.dropdown-toggle').click
-
-            click_button(by, class: 'dropdown-item')
-
-            find('.sorting-direction-button').click if direction == :ascending
-          end
-        end
-
-        shared_examples 'releases sort order' do
-          it "sorts the releases #{description}" do
-            card_titles = page.all('.release-block .card-title', minimum: expected_releases.count)
-
-            card_titles.each_with_index do |title, index|
-              expect(title).to have_content(expected_releases[index].name)
-            end
-          end
-        end
+    context 'sorting' do
+      def sort_page(by:, direction:)
+        within '[data-testid="releases-sort"]' do
+          find('.dropdown-toggle').click
 
-        context "when the page is sorted by the default sort order" do
-          let(:expected_releases) { [release_v3, release_v2, release_v1] }
+          click_button(by, class: 'dropdown-item')
 
-          it_behaves_like 'releases sort order'
+          find('.sorting-direction-button').click if direction == :ascending
         end
+      end
 
-        context "when the page is sorted by created_at ascending " do
-          let(:expected_releases) { [release_v2, release_v1, release_v3] }
+      shared_examples 'releases sort order' do
+        it "sorts the releases #{description}" do
+          card_titles = page.all('.release-block .card-title', minimum: expected_releases.count)
 
-          before do
-            sort_page by: 'Created date', direction: :ascending
+          card_titles.each_with_index do |title, index|
+            expect(title).to have_content(expected_releases[index].name)
           end
-
-          it_behaves_like 'releases sort order'
         end
       end
-    end
 
-    context('when the user is a guest') do
-      before do
-        sign_in(guest)
-      end
+      context "when the page is sorted by the default sort order" do
+        let(:expected_releases) { [release_v3, release_v2, release_v1] }
 
-      it 'renders release info except for Git-related data' do
-        visit project_releases_path(project)
+        it_behaves_like 'releases sort order'
+      end
 
-        within('.release-block', match: :first) do
-          expect(page).to have_content(release_v3.description)
-          expect(page).to have_content(release_v3.tag)
-          expect(page).to have_content(release_v3.name)
+      context "when the page is sorted by created_at ascending " do
+        let(:expected_releases) { [release_v2, release_v1, release_v3] }
 
-          # The following properties (sometimes) include Git info,
-          # so they are not rendered for Guest users
-          expect(page).not_to have_content(release_v3.commit.short_id)
+        before do
+          sort_page by: 'Created date', direction: :ascending
         end
+
+        it_behaves_like 'releases sort order'
       end
     end
   end
 
-  context 'when the releases_index_apollo_client feature flag is enabled' do
+  context('when the user is a guest') do
     before do
-      stub_feature_flags(releases_index_apollo_client: true)
+      sign_in(guest)
     end
 
-    it_behaves_like 'releases index page'
-  end
+    it 'renders release info except for Git-related data' do
+      visit project_releases_path(project)
 
-  context 'when the releases_index_apollo_client feature flag is disabled' do
-    before do
-      stub_feature_flags(releases_index_apollo_client: false)
-    end
+      within('.release-block', match: :first) do
+        expect(page).to have_content(release_v3.description)
+        expect(page).to have_content(release_v3.tag)
+        expect(page).to have_content(release_v3.name)
 
-    it_behaves_like 'releases index page'
+        # The following properties (sometimes) include Git info,
+        # so they are not rendered for Guest users
+        expect(page).not_to have_content(release_v3.commit.short_id)
+      end
+    end
   end
 end
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
deleted file mode 100644
index 9881ef9bc9f06cdca56799f7d285f36fee3a75db..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ /dev/null
@@ -1,398 +0,0 @@
-import { cloneDeep } from 'lodash';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
-import createFlash from '~/flash';
-import { historyPushState } from '~/lib/utils/common_utils';
-import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo_client.vue';
-import ReleaseBlock from '~/releases/components/release_block.vue';
-import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
-import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
-import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
-import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
-import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
-
-Vue.use(VueApollo);
-
-jest.mock('~/flash');
-
-let mockQueryParams;
-jest.mock('~/lib/utils/common_utils', () => ({
-  ...jest.requireActual('~/lib/utils/common_utils'),
-  historyPushState: jest.fn(),
-}));
-
-jest.mock('~/lib/utils/url_utility', () => ({
-  ...jest.requireActual('~/lib/utils/url_utility'),
-  getParameterByName: jest
-    .fn()
-    .mockImplementation((parameterName) => mockQueryParams[parameterName]),
-}));
-
-describe('app_index_apollo_client.vue', () => {
-  const projectPath = 'project/path';
-  const newReleasePath = 'path/to/new/release/page';
-  const before = 'beforeCursor';
-  const after = 'afterCursor';
-
-  let wrapper;
-  let allReleases;
-  let singleRelease;
-  let noReleases;
-  let queryMock;
-
-  const createComponent = ({
-    singleResponse = Promise.resolve(singleRelease),
-    fullResponse = Promise.resolve(allReleases),
-  } = {}) => {
-    const apolloProvider = createMockApollo([
-      [
-        allReleasesQuery,
-        queryMock.mockImplementation((vars) => {
-          return vars.first === 1 ? singleResponse : fullResponse;
-        }),
-      ],
-    ]);
-
-    wrapper = shallowMountExtended(ReleasesIndexApolloClientApp, {
-      apolloProvider,
-      provide: {
-        newReleasePath,
-        projectPath,
-      },
-    });
-  };
-
-  beforeEach(() => {
-    mockQueryParams = {};
-
-    allReleases = cloneDeep(originalAllReleasesQueryResponse);
-
-    singleRelease = cloneDeep(originalAllReleasesQueryResponse);
-    singleRelease.data.project.releases.nodes.splice(
-      1,
-      singleRelease.data.project.releases.nodes.length,
-    );
-
-    noReleases = cloneDeep(originalAllReleasesQueryResponse);
-    noReleases.data.project.releases.nodes = [];
-
-    queryMock = jest.fn();
-  });
-
-  afterEach(() => {
-    wrapper.destroy();
-  });
-
-  // Finders
-  const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
-  const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
-  const findNewReleaseButton = () =>
-    wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
-  const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
-  const findPagination = () => wrapper.findComponent(ReleasesPaginationApolloClient);
-  const findSort = () => wrapper.findComponent(ReleasesSortApolloClient);
-
-  // Tests
-  describe('component states', () => {
-    // These need to be defined as functions, since `singleRelease` and
-    // `allReleases` are generated in a `beforeEach`, and therefore
-    // aren't available at test definition time.
-    const getInProgressResponse = () => new Promise(() => {});
-    const getErrorResponse = () => Promise.reject(new Error('Oops!'));
-    const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
-    const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
-    const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
-
-    const toDescription = (bool) => (bool ? 'does' : 'does not');
-
-    describe.each`
-      description                                                       | singleResponseFn                  | fullResponseFn                  | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
-      ${'both requests loading'}                                        | ${getInProgressResponse}          | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
-      ${'both requests failed'}                                         | ${getErrorResponse}               | ${getErrorResponse}             | ${false}         | ${false}   | ${true}      | ${0}         | ${false}
-      ${'both requests loaded'}                                         | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
-      ${'both requests loaded with no results'}                         | ${getLoadedEmptyResponse}         | ${getLoadedEmptyResponse}       | ${false}         | ${true}    | ${false}     | ${0}         | ${false}
-      ${'single request loading, full request loaded'}                  | ${getInProgressResponse}          | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
-      ${'single request loading, full request failed'}                  | ${getInProgressResponse}          | ${getErrorResponse}             | ${true}          | ${false}   | ${true}      | ${0}         | ${false}
-      ${'single request loaded, full request loading'}                  | ${getSingleRequestLoadedResponse} | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${1}         | ${false}
-      ${'single request loaded, full request failed'}                   | ${getSingleRequestLoadedResponse} | ${getErrorResponse}             | ${false}         | ${false}   | ${true}      | ${1}         | ${false}
-      ${'single request failed, full request loading'}                  | ${getErrorResponse}               | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
-      ${'single request failed, full request loaded'}                   | ${getErrorResponse}               | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
-      ${'single request loaded with no results, full request loading'}  | ${getLoadedEmptyResponse}         | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
-      ${'single request loading, full request loadied with no results'} | ${getInProgressResponse}          | ${getLoadedEmptyResponse}       | ${false}         | ${true}    | ${false}     | ${0}         | ${false}
-    `(
-      '$description',
-      ({
-        singleResponseFn,
-        fullResponseFn,
-        loadingIndicator,
-        emptyState,
-        flashMessage,
-        releaseCount,
-        pagination,
-      }) => {
-        beforeEach(() => {
-          createComponent({
-            singleResponse: singleResponseFn(),
-            fullResponse: fullResponseFn(),
-          });
-        });
-
-        it(`${toDescription(loadingIndicator)} render a loading indicator`, async () => {
-          await waitForPromises();
-          expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
-        });
-
-        it(`${toDescription(emptyState)} render an empty state`, () => {
-          expect(findEmptyState().exists()).toBe(emptyState);
-        });
-
-        it(`${toDescription(flashMessage)} show a flash message`, () => {
-          if (flashMessage) {
-            expect(createFlash).toHaveBeenCalledWith({
-              message: ReleasesIndexApolloClientApp.i18n.errorMessage,
-              captureError: true,
-              error: expect.any(Error),
-            });
-          } else {
-            expect(createFlash).not.toHaveBeenCalled();
-          }
-        });
-
-        it(`renders ${releaseCount} release(s)`, () => {
-          expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
-        });
-
-        it(`${toDescription(pagination)} render the pagination controls`, () => {
-          expect(findPagination().exists()).toBe(pagination);
-        });
-
-        it('does render the "New release" button', () => {
-          expect(findNewReleaseButton().exists()).toBe(true);
-        });
-
-        it('does render the sort controls', () => {
-          expect(findSort().exists()).toBe(true);
-        });
-      },
-    );
-  });
-
-  describe('URL parameters', () => {
-    describe('when the URL contains no query parameters', () => {
-      beforeEach(() => {
-        createComponent();
-      });
-
-      it('makes a request with the correct GraphQL query parameters', () => {
-        expect(queryMock).toHaveBeenCalledTimes(2);
-
-        expect(queryMock).toHaveBeenCalledWith({
-          first: 1,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-
-        expect(queryMock).toHaveBeenCalledWith({
-          first: PAGE_SIZE,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-      });
-    });
-
-    describe('when the URL contains a "before" query parameter', () => {
-      beforeEach(() => {
-        mockQueryParams = { before };
-        createComponent();
-      });
-
-      it('makes a request with the correct GraphQL query parameters', () => {
-        expect(queryMock).toHaveBeenCalledTimes(1);
-
-        expect(queryMock).toHaveBeenCalledWith({
-          before,
-          last: PAGE_SIZE,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-      });
-    });
-
-    describe('when the URL contains an "after" query parameter', () => {
-      beforeEach(() => {
-        mockQueryParams = { after };
-        createComponent();
-      });
-
-      it('makes a request with the correct GraphQL query parameters', () => {
-        expect(queryMock).toHaveBeenCalledTimes(2);
-
-        expect(queryMock).toHaveBeenCalledWith({
-          after,
-          first: 1,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-
-        expect(queryMock).toHaveBeenCalledWith({
-          after,
-          first: PAGE_SIZE,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-      });
-    });
-
-    describe('when the URL contains both "before" and "after" query parameters', () => {
-      beforeEach(() => {
-        mockQueryParams = { before, after };
-        createComponent();
-      });
-
-      it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
-        expect(queryMock).toHaveBeenCalledTimes(2);
-
-        expect(queryMock).toHaveBeenCalledWith({
-          after,
-          first: 1,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-
-        expect(queryMock).toHaveBeenCalledWith({
-          after,
-          first: PAGE_SIZE,
-          fullPath: projectPath,
-          sort: DEFAULT_SORT,
-        });
-      });
-    });
-  });
-
-  describe('New release button', () => {
-    beforeEach(() => {
-      createComponent();
-    });
-
-    it('renders the new release button with the correct href', () => {
-      expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
-    });
-  });
-
-  describe('pagination', () => {
-    beforeEach(() => {
-      mockQueryParams = { before };
-      createComponent();
-    });
-
-    it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
-      expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
-
-      mockQueryParams = { after };
-      findPagination().vm.$emit('next', after);
-
-      await nextTick();
-
-      expect(queryMock.mock.calls).toEqual([
-        [expect.objectContaining({ before })],
-        [expect.objectContaining({ after })],
-        [expect.objectContaining({ after })],
-      ]);
-    });
-  });
-
-  describe('sorting', () => {
-    beforeEach(() => {
-      createComponent();
-    });
-
-    it(`sorts by ${DEFAULT_SORT} by default`, () => {
-      expect(queryMock.mock.calls).toEqual([
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-      ]);
-    });
-
-    it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
-      findSort().vm.$emit('input', CREATED_ASC);
-
-      await nextTick();
-
-      expect(queryMock.mock.calls).toEqual([
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-        [expect.objectContaining({ sort: CREATED_ASC })],
-        [expect.objectContaining({ sort: CREATED_ASC })],
-      ]);
-
-      // URL manipulation is tested in more detail in the `describe` block below
-      expect(historyPushState).toHaveBeenCalled();
-    });
-
-    it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
-      findSort().vm.$emit('input', DEFAULT_SORT);
-
-      await nextTick();
-
-      expect(queryMock.mock.calls).toEqual([
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-        [expect.objectContaining({ sort: DEFAULT_SORT })],
-      ]);
-
-      expect(historyPushState).not.toHaveBeenCalled();
-    });
-  });
-
-  describe('sorting + pagination interaction', () => {
-    const nonPaginationQueryParam = 'nonPaginationQueryParam';
-
-    beforeEach(() => {
-      historyPushState.mockImplementation((newUrl) => {
-        mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
-      });
-    });
-
-    describe.each`
-      queryParamsBefore                      | paramName   | paramInitialValue
-      ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
-      ${{ after, nonPaginationQueryParam }}  | ${'after'}  | ${after}
-    `(
-      'when the URL contains a "$paramName" pagination cursor',
-      ({ queryParamsBefore, paramName, paramInitialValue }) => {
-        beforeEach(async () => {
-          mockQueryParams = queryParamsBefore;
-          createComponent();
-
-          findSort().vm.$emit('input', CREATED_ASC);
-
-          await nextTick();
-        });
-
-        it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
-          const firstRequestVariables = queryMock.mock.calls[0][0];
-          // Might be request #2 or #3, depending on the pagination direction
-          const mostRecentRequestVariables =
-            queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
-
-          expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
-          expect(mostRecentRequestVariables[paramName]).toBeUndefined();
-        });
-
-        it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
-          expect(historyPushState).toHaveBeenCalledTimes(1);
-
-          const updatedUrlQueryParams = Object.fromEntries(
-            new URL(historyPushState.mock.calls[0][0]).searchParams,
-          );
-
-          expect(updatedUrlQueryParams[paramName]).toBeUndefined();
-        });
-      },
-    );
-  });
-});
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 43e88650ae3986e6c2996a1ffdd44cb699e4ad5c..0d376acf1ae7dafdac3e869970bde45baf71054a 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,50 +1,87 @@
-import { shallowMount } from '@vue/test-utils';
-import { merge } from 'lodash';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { getParameterByName } from '~/lib/utils/url_utility';
-import AppIndex from '~/releases/components/app_index.vue';
+import { cloneDeep } from 'lodash';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+import createFlash from '~/flash';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesIndexApp from '~/releases/components/app_index.vue';
+import ReleaseBlock from '~/releases/components/release_block.vue';
 import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
+import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
 import ReleasesPagination from '~/releases/components/releases_pagination.vue';
 import ReleasesSort from '~/releases/components/releases_sort.vue';
+import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+let mockQueryParams;
+jest.mock('~/lib/utils/common_utils', () => ({
+  ...jest.requireActual('~/lib/utils/common_utils'),
+  historyPushState: jest.fn(),
+}));
 
 jest.mock('~/lib/utils/url_utility', () => ({
   ...jest.requireActual('~/lib/utils/url_utility'),
-  getParameterByName: jest.fn(),
+  getParameterByName: jest
+    .fn()
+    .mockImplementation((parameterName) => mockQueryParams[parameterName]),
 }));
 
-Vue.use(Vuex);
-
 describe('app_index.vue', () => {
+  const projectPath = 'project/path';
+  const newReleasePath = 'path/to/new/release/page';
+  const before = 'beforeCursor';
+  const after = 'afterCursor';
+
   let wrapper;
-  let fetchReleasesSpy;
-  let urlParams;
-
-  const createComponent = (storeUpdates) => {
-    wrapper = shallowMount(AppIndex, {
-      store: new Vuex.Store({
-        modules: {
-          index: merge(
-            {
-              namespaced: true,
-              actions: {
-                fetchReleases: fetchReleasesSpy,
-              },
-              state: {
-                isLoading: true,
-                releases: [],
-              },
-            },
-            storeUpdates,
-          ),
-        },
-      }),
+  let allReleases;
+  let singleRelease;
+  let noReleases;
+  let queryMock;
+
+  const createComponent = ({
+    singleResponse = Promise.resolve(singleRelease),
+    fullResponse = Promise.resolve(allReleases),
+  } = {}) => {
+    const apolloProvider = createMockApollo([
+      [
+        allReleasesQuery,
+        queryMock.mockImplementation((vars) => {
+          return vars.first === 1 ? singleResponse : fullResponse;
+        }),
+      ],
+    ]);
+
+    wrapper = shallowMountExtended(ReleasesIndexApp, {
+      apolloProvider,
+      provide: {
+        newReleasePath,
+        projectPath,
+      },
     });
   };
 
   beforeEach(() => {
-    fetchReleasesSpy = jest.fn();
-    getParameterByName.mockImplementation((paramName) => urlParams[paramName]);
+    mockQueryParams = {};
+
+    allReleases = cloneDeep(originalAllReleasesQueryResponse);
+
+    singleRelease = cloneDeep(originalAllReleasesQueryResponse);
+    singleRelease.data.project.releases.nodes.splice(
+      1,
+      singleRelease.data.project.releases.nodes.length,
+    );
+
+    noReleases = cloneDeep(originalAllReleasesQueryResponse);
+    noReleases.data.project.releases.nodes = [];
+
+    queryMock = jest.fn();
   });
 
   afterEach(() => {
@@ -52,120 +89,220 @@ describe('app_index.vue', () => {
   });
 
   // Finders
-  const findLoadingIndicator = () => wrapper.find(ReleaseSkeletonLoader);
-  const findEmptyState = () => wrapper.find('[data-testid="empty-state"]');
-  const findSuccessState = () => wrapper.find('[data-testid="success-state"]');
-  const findPagination = () => wrapper.find(ReleasesPagination);
-  const findSortControls = () => wrapper.find(ReleasesSort);
-  const findNewReleaseButton = () => wrapper.find('[data-testid="new-release-button"]');
-
-  // Expectations
-  const expectLoadingIndicator = (shouldExist) => {
-    it(`${shouldExist ? 'renders' : 'does not render'} a loading indicator`, () => {
-      expect(findLoadingIndicator().exists()).toBe(shouldExist);
-    });
-  };
+  const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
+  const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
+  const findNewReleaseButton = () => wrapper.findByText(ReleasesIndexApp.i18n.newRelease);
+  const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
+  const findPagination = () => wrapper.findComponent(ReleasesPagination);
+  const findSort = () => wrapper.findComponent(ReleasesSort);
 
-  const expectEmptyState = (shouldExist) => {
-    it(`${shouldExist ? 'renders' : 'does not render'} an empty state`, () => {
-      expect(findEmptyState().exists()).toBe(shouldExist);
-    });
-  };
+  // Tests
+  describe('component states', () => {
+    // These need to be defined as functions, since `singleRelease` and
+    // `allReleases` are generated in a `beforeEach`, and therefore
+    // aren't available at test definition time.
+    const getInProgressResponse = () => new Promise(() => {});
+    const getErrorResponse = () => Promise.reject(new Error('Oops!'));
+    const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
+    const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
+    const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
+
+    const toDescription = (bool) => (bool ? 'does' : 'does not');
+
+    describe.each`
+      description                                                       | singleResponseFn                  | fullResponseFn                  | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+      ${'both requests loading'}                                        | ${getInProgressResponse}          | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
+      ${'both requests failed'}                                         | ${getErrorResponse}               | ${getErrorResponse}             | ${false}         | ${false}   | ${true}      | ${0}         | ${false}
+      ${'both requests loaded'}                                         | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
+      ${'both requests loaded with no results'}                         | ${getLoadedEmptyResponse}         | ${getLoadedEmptyResponse}       | ${false}         | ${true}    | ${false}     | ${0}         | ${false}
+      ${'single request loading, full request loaded'}                  | ${getInProgressResponse}          | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
+      ${'single request loading, full request failed'}                  | ${getInProgressResponse}          | ${getErrorResponse}             | ${true}          | ${false}   | ${true}      | ${0}         | ${false}
+      ${'single request loaded, full request loading'}                  | ${getSingleRequestLoadedResponse} | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${1}         | ${false}
+      ${'single request loaded, full request failed'}                   | ${getSingleRequestLoadedResponse} | ${getErrorResponse}             | ${false}         | ${false}   | ${true}      | ${1}         | ${false}
+      ${'single request failed, full request loading'}                  | ${getErrorResponse}               | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
+      ${'single request failed, full request loaded'}                   | ${getErrorResponse}               | ${getFullRequestLoadedResponse} | ${false}         | ${false}   | ${false}     | ${2}         | ${true}
+      ${'single request loaded with no results, full request loading'}  | ${getLoadedEmptyResponse}         | ${getInProgressResponse}        | ${true}          | ${false}   | ${false}     | ${0}         | ${false}
+      ${'single request loading, full request loadied with no results'} | ${getInProgressResponse}          | ${getLoadedEmptyResponse}       | ${false}         | ${true}    | ${false}     | ${0}         | ${false}
+    `(
+      '$description',
+      ({
+        singleResponseFn,
+        fullResponseFn,
+        loadingIndicator,
+        emptyState,
+        flashMessage,
+        releaseCount,
+        pagination,
+      }) => {
+        beforeEach(() => {
+          createComponent({
+            singleResponse: singleResponseFn(),
+            fullResponse: fullResponseFn(),
+          });
+        });
+
+        it(`${toDescription(loadingIndicator)} render a loading indicator`, async () => {
+          await waitForPromises();
+          expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
+        });
+
+        it(`${toDescription(emptyState)} render an empty state`, () => {
+          expect(findEmptyState().exists()).toBe(emptyState);
+        });
+
+        it(`${toDescription(flashMessage)} show a flash message`, () => {
+          if (flashMessage) {
+            expect(createFlash).toHaveBeenCalledWith({
+              message: ReleasesIndexApp.i18n.errorMessage,
+              captureError: true,
+              error: expect.any(Error),
+            });
+          } else {
+            expect(createFlash).not.toHaveBeenCalled();
+          }
+        });
+
+        it(`renders ${releaseCount} release(s)`, () => {
+          expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
+        });
+
+        it(`${toDescription(pagination)} render the pagination controls`, () => {
+          expect(findPagination().exists()).toBe(pagination);
+        });
+
+        it('does render the "New release" button', () => {
+          expect(findNewReleaseButton().exists()).toBe(true);
+        });
+
+        it('does render the sort controls', () => {
+          expect(findSort().exists()).toBe(true);
+        });
+      },
+    );
+  });
 
-  const expectSuccessState = (shouldExist) => {
-    it(`${shouldExist ? 'renders' : 'does not render'} the success state`, () => {
-      expect(findSuccessState().exists()).toBe(shouldExist);
-    });
-  };
+  describe('URL parameters', () => {
+    describe('when the URL contains no query parameters', () => {
+      beforeEach(() => {
+        createComponent();
+      });
 
-  const expectPagination = (shouldExist) => {
-    it(`${shouldExist ? 'renders' : 'does not render'} the pagination controls`, () => {
-      expect(findPagination().exists()).toBe(shouldExist);
-    });
-  };
+      it('makes a request with the correct GraphQL query parameters', () => {
+        expect(queryMock).toHaveBeenCalledTimes(2);
 
-  const expectNewReleaseButton = (shouldExist) => {
-    it(`${shouldExist ? 'renders' : 'does not render'} the "New release" button`, () => {
-      expect(findNewReleaseButton().exists()).toBe(shouldExist);
-    });
-  };
+        expect(queryMock).toHaveBeenCalledWith({
+          first: 1,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
 
-  // Tests
-  describe('on startup', () => {
-    it.each`
-      before                  | after
-      ${null}                 | ${null}
-      ${'before_param_value'} | ${null}
-      ${null}                 | ${'after_param_value'}
-    `(
-      'calls fetchRelease with the correct parameters based on the curent query parameters: before: $before, after: $after',
-      ({ before, after }) => {
-        urlParams = { before, after };
+        expect(queryMock).toHaveBeenCalledWith({
+          first: PAGE_SIZE,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
+      });
+    });
 
+    describe('when the URL contains a "before" query parameter', () => {
+      beforeEach(() => {
+        mockQueryParams = { before };
         createComponent();
+      });
 
-        expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
-        expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
-      },
-    );
-  });
+      it('makes a request with the correct GraphQL query parameters', () => {
+        expect(queryMock).toHaveBeenCalledTimes(1);
 
-  describe('when the request to fetch releases has not yet completed', () => {
-    beforeEach(() => {
-      createComponent();
+        expect(queryMock).toHaveBeenCalledWith({
+          before,
+          last: PAGE_SIZE,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
+      });
     });
 
-    expectLoadingIndicator(true);
-    expectEmptyState(false);
-    expectSuccessState(false);
-    expectPagination(false);
-  });
+    describe('when the URL contains an "after" query parameter', () => {
+      beforeEach(() => {
+        mockQueryParams = { after };
+        createComponent();
+      });
 
-  describe('when the request fails', () => {
-    beforeEach(() => {
-      createComponent({
-        state: {
-          isLoading: false,
-          hasError: true,
-        },
+      it('makes a request with the correct GraphQL query parameters', () => {
+        expect(queryMock).toHaveBeenCalledTimes(2);
+
+        expect(queryMock).toHaveBeenCalledWith({
+          after,
+          first: 1,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
+
+        expect(queryMock).toHaveBeenCalledWith({
+          after,
+          first: PAGE_SIZE,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
       });
     });
 
-    expectLoadingIndicator(false);
-    expectEmptyState(false);
-    expectSuccessState(false);
-    expectPagination(true);
+    describe('when the URL contains both "before" and "after" query parameters', () => {
+      beforeEach(() => {
+        mockQueryParams = { before, after };
+        createComponent();
+      });
+
+      it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
+        expect(queryMock).toHaveBeenCalledTimes(2);
+
+        expect(queryMock).toHaveBeenCalledWith({
+          after,
+          first: 1,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
+
+        expect(queryMock).toHaveBeenCalledWith({
+          after,
+          first: PAGE_SIZE,
+          fullPath: projectPath,
+          sort: DEFAULT_SORT,
+        });
+      });
+    });
   });
 
-  describe('when the request succeeds but returns no releases', () => {
+  describe('New release button', () => {
     beforeEach(() => {
-      createComponent({
-        state: {
-          isLoading: false,
-        },
-      });
+      createComponent();
     });
 
-    expectLoadingIndicator(false);
-    expectEmptyState(true);
-    expectSuccessState(false);
-    expectPagination(true);
+    it('renders the new release button with the correct href', () => {
+      expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
+    });
   });
 
-  describe('when the request succeeds and includes at least one release', () => {
+  describe('pagination', () => {
     beforeEach(() => {
-      createComponent({
-        state: {
-          isLoading: false,
-          releases: [{}],
-        },
-      });
+      mockQueryParams = { before };
+      createComponent();
     });
 
-    expectLoadingIndicator(false);
-    expectEmptyState(false);
-    expectSuccessState(true);
-    expectPagination(true);
+    it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
+      expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
+
+      mockQueryParams = { after };
+      findPagination().vm.$emit('next', after);
+
+      await nextTick();
+
+      expect(queryMock.mock.calls).toEqual([
+        [expect.objectContaining({ before })],
+        [expect.objectContaining({ after })],
+        [expect.objectContaining({ after })],
+      ]);
+    });
   });
 
   describe('sorting', () => {
@@ -173,59 +310,88 @@ describe('app_index.vue', () => {
       createComponent();
     });
 
-    it('renders the sort controls', () => {
-      expect(findSortControls().exists()).toBe(true);
+    it(`sorts by ${DEFAULT_SORT} by default`, () => {
+      expect(queryMock.mock.calls).toEqual([
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+      ]);
     });
 
-    it('calls the fetchReleases store method when the sort is updated', () => {
-      fetchReleasesSpy.mockClear();
+    it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
+      findSort().vm.$emit('input', CREATED_ASC);
+
+      await nextTick();
 
-      findSortControls().vm.$emit('sort:changed');
+      expect(queryMock.mock.calls).toEqual([
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+        [expect.objectContaining({ sort: CREATED_ASC })],
+        [expect.objectContaining({ sort: CREATED_ASC })],
+      ]);
 
-      expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
+      // URL manipulation is tested in more detail in the `describe` block below
+      expect(historyPushState).toHaveBeenCalled();
     });
-  });
 
-  describe('"New release" button', () => {
-    describe('when the user is allowed to create releases', () => {
-      const newReleasePath = 'path/to/new/release/page';
+    it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
+      findSort().vm.$emit('input', DEFAULT_SORT);
 
-      beforeEach(() => {
-        createComponent({ state: { newReleasePath } });
-      });
+      await nextTick();
 
-      expectNewReleaseButton(true);
+      expect(queryMock.mock.calls).toEqual([
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+        [expect.objectContaining({ sort: DEFAULT_SORT })],
+      ]);
 
-      it('renders the button with the correct href', () => {
-        expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath);
-      });
+      expect(historyPushState).not.toHaveBeenCalled();
     });
+  });
 
-    describe('when the user is not allowed to create releases', () => {
-      beforeEach(() => {
-        createComponent();
-      });
+  describe('sorting + pagination interaction', () => {
+    const nonPaginationQueryParam = 'nonPaginationQueryParam';
 
-      expectNewReleaseButton(false);
+    beforeEach(() => {
+      historyPushState.mockImplementation((newUrl) => {
+        mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
+      });
     });
-  });
 
-  describe("when the browser's back button is pressed", () => {
-    beforeEach(() => {
-      urlParams = {
-        before: 'before_param_value',
-      };
+    describe.each`
+      queryParamsBefore                      | paramName   | paramInitialValue
+      ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
+      ${{ after, nonPaginationQueryParam }}  | ${'after'}  | ${after}
+    `(
+      'when the URL contains a "$paramName" pagination cursor',
+      ({ queryParamsBefore, paramName, paramInitialValue }) => {
+        beforeEach(async () => {
+          mockQueryParams = queryParamsBefore;
+          createComponent();
 
-      createComponent();
+          findSort().vm.$emit('input', CREATED_ASC);
 
-      fetchReleasesSpy.mockClear();
+          await nextTick();
+        });
 
-      window.dispatchEvent(new PopStateEvent('popstate'));
-    });
+        it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
+          const firstRequestVariables = queryMock.mock.calls[0][0];
+          // Might be request #2 or #3, depending on the pagination direction
+          const mostRecentRequestVariables =
+            queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
 
-    it('calls the fetchRelease store method with the parameters from the URL query', () => {
-      expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
-      expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
-    });
+          expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
+          expect(mostRecentRequestVariables[paramName]).toBeUndefined();
+        });
+
+        it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
+          expect(historyPushState).toHaveBeenCalledTimes(1);
+
+          const updatedUrlQueryParams = Object.fromEntries(
+            new URL(historyPushState.mock.calls[0][0]).searchParams,
+          );
+
+          expect(updatedUrlQueryParams[paramName]).toBeUndefined();
+        });
+      },
+    );
   });
 });
diff --git a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
deleted file mode 100644
index a538afd5d38be980435226e1f10780138cf4cb1f..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { historyPushState } from '~/lib/utils/common_utils';
-import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
-
-jest.mock('~/lib/utils/common_utils', () => ({
-  ...jest.requireActual('~/lib/utils/common_utils'),
-  historyPushState: jest.fn(),
-}));
-
-describe('releases_pagination_apollo_client.vue', () => {
-  const startCursor = 'startCursor';
-  const endCursor = 'endCursor';
-  let wrapper;
-  let onPrev;
-  let onNext;
-
-  const createComponent = (pageInfo) => {
-    onPrev = jest.fn();
-    onNext = jest.fn();
-
-    wrapper = mountExtended(ReleasesPaginationApolloClient, {
-      propsData: {
-        pageInfo,
-      },
-      listeners: {
-        prev: onPrev,
-        next: onNext,
-      },
-    });
-  };
-
-  afterEach(() => {
-    wrapper.destroy();
-  });
-
-  const singlePageInfo = {
-    hasPreviousPage: false,
-    hasNextPage: false,
-    startCursor,
-    endCursor,
-  };
-
-  const onlyNextPageInfo = {
-    hasPreviousPage: false,
-    hasNextPage: true,
-    startCursor,
-    endCursor,
-  };
-
-  const onlyPrevPageInfo = {
-    hasPreviousPage: true,
-    hasNextPage: false,
-    startCursor,
-    endCursor,
-  };
-
-  const prevAndNextPageInfo = {
-    hasPreviousPage: true,
-    hasNextPage: true,
-    startCursor,
-    endCursor,
-  };
-
-  const findPrevButton = () => wrapper.findByTestId('prevButton');
-  const findNextButton = () => wrapper.findByTestId('nextButton');
-
-  describe.each`
-    description                                             | pageInfo               | prevEnabled | nextEnabled
-    ${'when there is only one page of results'}             | ${singlePageInfo}      | ${false}    | ${false}
-    ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo}    | ${false}    | ${true}
-    ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo}    | ${true}     | ${false}
-    ${'when there is both a previous and next page'}        | ${prevAndNextPageInfo} | ${true}     | ${true}
-  `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
-    describe(description, () => {
-      beforeEach(() => {
-        createComponent(pageInfo);
-      });
-
-      it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
-        expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
-      });
-
-      it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
-        expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
-      });
-    });
-  });
-
-  describe('button behavior', () => {
-    beforeEach(() => {
-      createComponent(prevAndNextPageInfo);
-    });
-
-    describe('next button behavior', () => {
-      beforeEach(() => {
-        findNextButton().trigger('click');
-      });
-
-      it('emits an "next" event with the "after" cursor', () => {
-        expect(onNext.mock.calls).toEqual([[endCursor]]);
-      });
-
-      it('calls historyPushState with the new URL', () => {
-        expect(historyPushState.mock.calls).toEqual([
-          [expect.stringContaining(`?after=${endCursor}`)],
-        ]);
-      });
-    });
-
-    describe('prev button behavior', () => {
-      beforeEach(() => {
-        findPrevButton().trigger('click');
-      });
-
-      it('emits an "prev" event with the "before" cursor', () => {
-        expect(onPrev.mock.calls).toEqual([[startCursor]]);
-      });
-
-      it('calls historyPushState with the new URL', () => {
-        expect(historyPushState.mock.calls).toEqual([
-          [expect.stringContaining(`?before=${startCursor}`)],
-        ]);
-      });
-    });
-  });
-});
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index b8c69b0ea70b4b51ba1ceff0606814be52de5f57..59be808c802616f3aad23198a3d8947479ca728f 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -1,140 +1,94 @@
-import { GlKeysetPagination } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
 import { historyPushState } from '~/lib/utils/common_utils';
 import ReleasesPagination from '~/releases/components/releases_pagination.vue';
-import createStore from '~/releases/stores';
-import createIndexModule from '~/releases/stores/modules/index';
 
 jest.mock('~/lib/utils/common_utils', () => ({
   ...jest.requireActual('~/lib/utils/common_utils'),
   historyPushState: jest.fn(),
 }));
 
-Vue.use(Vuex);
-
-describe('~/releases/components/releases_pagination.vue', () => {
+describe('releases_pagination.vue', () => {
+  const startCursor = 'startCursor';
+  const endCursor = 'endCursor';
   let wrapper;
-  let indexModule;
-
-  const cursors = {
-    startCursor: 'startCursor',
-    endCursor: 'endCursor',
-  };
-
-  const projectPath = 'my/project';
+  let onPrev;
+  let onNext;
 
   const createComponent = (pageInfo) => {
-    indexModule = createIndexModule({ projectPath });
-
-    indexModule.state.pageInfo = pageInfo;
-
-    indexModule.actions.fetchReleases = jest.fn();
-
-    wrapper = mount(ReleasesPagination, {
-      store: createStore({
-        modules: {
-          index: indexModule,
-        },
-        featureFlags: {},
-      }),
+    onPrev = jest.fn();
+    onNext = jest.fn();
+
+    wrapper = mountExtended(ReleasesPagination, {
+      propsData: {
+        pageInfo,
+      },
+      listeners: {
+        prev: onPrev,
+        next: onNext,
+      },
     });
   };
 
   afterEach(() => {
     wrapper.destroy();
-    wrapper = null;
   });
 
-  const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
-  const findPrevButton = () => findGlKeysetPagination().find('[data-testid="prevButton"]');
-  const findNextButton = () => findGlKeysetPagination().find('[data-testid="nextButton"]');
-
-  const expectDisabledPrev = () => {
-    expect(findPrevButton().attributes().disabled).toBe('disabled');
+  const singlePageInfo = {
+    hasPreviousPage: false,
+    hasNextPage: false,
+    startCursor,
+    endCursor,
   };
-  const expectEnabledPrev = () => {
-    expect(findPrevButton().attributes().disabled).toBe(undefined);
+
+  const onlyNextPageInfo = {
+    hasPreviousPage: false,
+    hasNextPage: true,
+    startCursor,
+    endCursor,
   };
-  const expectDisabledNext = () => {
-    expect(findNextButton().attributes().disabled).toBe('disabled');
+
+  const onlyPrevPageInfo = {
+    hasPreviousPage: true,
+    hasNextPage: false,
+    startCursor,
+    endCursor,
   };
-  const expectEnabledNext = () => {
-    expect(findNextButton().attributes().disabled).toBe(undefined);
+
+  const prevAndNextPageInfo = {
+    hasPreviousPage: true,
+    hasNextPage: true,
+    startCursor,
+    endCursor,
   };
 
-  describe('when there is only one page of results', () => {
-    beforeEach(() => {
-      createComponent({
-        hasPreviousPage: false,
-        hasNextPage: false,
+  const findPrevButton = () => wrapper.findByTestId('prevButton');
+  const findNextButton = () => wrapper.findByTestId('nextButton');
+
+  describe.each`
+    description                                             | pageInfo               | prevEnabled | nextEnabled
+    ${'when there is only one page of results'}             | ${singlePageInfo}      | ${false}    | ${false}
+    ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo}    | ${false}    | ${true}
+    ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo}    | ${true}     | ${false}
+    ${'when there is both a previous and next page'}        | ${prevAndNextPageInfo} | ${true}     | ${true}
+  `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
+    describe(description, () => {
+      beforeEach(() => {
+        createComponent(pageInfo);
       });
-    });
-
-    it('does not render a GlKeysetPagination', () => {
-      expect(findGlKeysetPagination().exists()).toBe(false);
-    });
-  });
 
-  describe('when there is a next page, but not a previous page', () => {
-    beforeEach(() => {
-      createComponent({
-        hasPreviousPage: false,
-        hasNextPage: true,
+      it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
+        expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
       });
-    });
-
-    it('renders a disabled "Prev" button', () => {
-      expectDisabledPrev();
-    });
 
-    it('renders an enabled "Next" button', () => {
-      expectEnabledNext();
-    });
-  });
-
-  describe('when there is a previous page, but not a next page', () => {
-    beforeEach(() => {
-      createComponent({
-        hasPreviousPage: true,
-        hasNextPage: false,
-      });
-    });
-
-    it('renders a enabled "Prev" button', () => {
-      expectEnabledPrev();
-    });
-
-    it('renders an disabled "Next" button', () => {
-      expectDisabledNext();
-    });
-  });
-
-  describe('when there is both a previous page and a next page', () => {
-    beforeEach(() => {
-      createComponent({
-        hasPreviousPage: true,
-        hasNextPage: true,
+      it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
+        expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
       });
     });
-
-    it('renders a enabled "Prev" button', () => {
-      expectEnabledPrev();
-    });
-
-    it('renders an enabled "Next" button', () => {
-      expectEnabledNext();
-    });
   });
 
   describe('button behavior', () => {
     beforeEach(() => {
-      createComponent({
-        hasPreviousPage: true,
-        hasNextPage: true,
-        ...cursors,
-      });
+      createComponent(prevAndNextPageInfo);
     });
 
     describe('next button behavior', () => {
@@ -142,33 +96,29 @@ describe('~/releases/components/releases_pagination.vue', () => {
         findNextButton().trigger('click');
       });
 
-      it('calls fetchReleases with the correct after cursor', () => {
-        expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
-          [expect.anything(), { after: cursors.endCursor }],
-        ]);
+      it('emits an "next" event with the "after" cursor', () => {
+        expect(onNext.mock.calls).toEqual([[endCursor]]);
       });
 
       it('calls historyPushState with the new URL', () => {
         expect(historyPushState.mock.calls).toEqual([
-          [expect.stringContaining(`?after=${cursors.endCursor}`)],
+          [expect.stringContaining(`?after=${endCursor}`)],
         ]);
       });
     });
 
-    describe('previous button behavior', () => {
+    describe('prev button behavior', () => {
       beforeEach(() => {
         findPrevButton().trigger('click');
       });
 
-      it('calls fetchReleases with the correct before cursor', () => {
-        expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
-          [expect.anything(), { before: cursors.startCursor }],
-        ]);
+      it('emits an "prev" event with the "before" cursor', () => {
+        expect(onPrev.mock.calls).toEqual([[startCursor]]);
       });
 
       it('calls historyPushState with the new URL', () => {
         expect(historyPushState.mock.calls).toEqual([
-          [expect.stringContaining(`?before=${cursors.startCursor}`)],
+          [expect.stringContaining(`?before=${startCursor}`)],
         ]);
       });
     });
diff --git a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
deleted file mode 100644
index d93a932af0133f76ca0ad9e0373a0989950678f0..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
-import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
-
-describe('releases_sort_apollo_client.vue', () => {
-  let wrapper;
-
-  const createComponent = (valueProp = RELEASED_AT_ASC) => {
-    wrapper = shallowMountExtended(ReleasesSortApolloClient, {
-      propsData: {
-        value: valueProp,
-      },
-      stubs: {
-        GlSortingItem,
-      },
-    });
-  };
-
-  afterEach(() => {
-    wrapper.destroy();
-  });
-
-  const findSorting = () => wrapper.findComponent(GlSorting);
-  const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
-  const findReleasedDateItem = () =>
-    findSortingItems().wrappers.find((item) => item.text() === 'Released date');
-  const findCreatedDateItem = () =>
-    findSortingItems().wrappers.find((item) => item.text() === 'Created date');
-  const getSortingItemsInfo = () =>
-    findSortingItems().wrappers.map((item) => ({
-      label: item.text(),
-      active: item.attributes().active === 'true',
-    }));
-
-  describe.each`
-    valueProp           | text               | isAscending | items
-    ${RELEASED_AT_ASC}  | ${'Released date'} | ${true}     | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
-    ${RELEASED_AT_DESC} | ${'Released date'} | ${false}    | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
-    ${CREATED_ASC}      | ${'Created date'}  | ${true}     | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
-    ${CREATED_DESC}     | ${'Created date'}  | ${false}    | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
-  `('component states', ({ valueProp, text, isAscending, items }) => {
-    beforeEach(() => {
-      createComponent(valueProp);
-    });
-
-    it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => {
-      expect(findSorting().props()).toEqual(
-        expect.objectContaining({
-          text,
-          isAscending,
-        }),
-      );
-    });
-
-    it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => {
-      expect(getSortingItemsInfo()).toEqual(items);
-    });
-  });
-
-  const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click');
-  const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click');
-  const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange');
-
-  const releasedAtDropdownItemDescription = 'released at dropdown item';
-  const createdAtDropdownItemDescription = 'created at dropdown item';
-  const sortDirectionButtonDescription = 'sort direction button';
-
-  describe.each`
-    initialValueProp    | itemClickFn                 | itemToClickDescription               | emittedEvent
-    ${RELEASED_AT_ASC}  | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${undefined}
-    ${RELEASED_AT_ASC}  | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${CREATED_ASC}
-    ${RELEASED_AT_ASC}  | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${RELEASED_AT_DESC}
-    ${RELEASED_AT_DESC} | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${undefined}
-    ${RELEASED_AT_DESC} | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${CREATED_DESC}
-    ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${RELEASED_AT_ASC}
-    ${CREATED_ASC}      | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC}
-    ${CREATED_ASC}      | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${undefined}
-    ${CREATED_ASC}      | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${CREATED_DESC}
-    ${CREATED_DESC}     | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC}
-    ${CREATED_DESC}     | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${undefined}
-    ${CREATED_DESC}     | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${CREATED_ASC}
-  `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => {
-    beforeEach(() => {
-      createComponent(initialValueProp);
-      itemClickFn();
-    });
-
-    it(`emits ${
-      emittedEvent || 'nothing'
-    } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => {
-      expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent);
-    });
-  });
-
-  describe('prop validation', () => {
-    it('validates that the `value` prop is one of the expected sort strings', () => {
-      expect(() => {
-        createComponent('not a valid value');
-      }).toThrow('Invalid prop: custom validator check failed');
-    });
-  });
-});
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index 7774532bc122135f8aadb68468a21678bd47f800..c6e1846d252b055847ed2232e6fc073a78f56e78 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,65 +1,103 @@
 import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import ReleasesSort from '~/releases/components/releases_sort.vue';
-import createStore from '~/releases/stores';
-import createIndexModule from '~/releases/stores/modules/index';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
 
-Vue.use(Vuex);
-
-describe('~/releases/components/releases_sort.vue', () => {
+describe('releases_sort.vue', () => {
   let wrapper;
-  let store;
-  let indexModule;
-  const projectId = 8;
-
-  const createComponent = () => {
-    indexModule = createIndexModule({ projectId });
 
-    store = createStore({
-      modules: {
-        index: indexModule,
+  const createComponent = (valueProp = RELEASED_AT_ASC) => {
+    wrapper = shallowMountExtended(ReleasesSort, {
+      propsData: {
+        value: valueProp,
       },
-    });
-
-    store.dispatch = jest.fn();
-
-    wrapper = shallowMount(ReleasesSort, {
-      store,
       stubs: {
         GlSortingItem,
       },
     });
   };
 
-  const findReleasesSorting = () => wrapper.find(GlSorting);
-  const findSortingItems = () => wrapper.findAll(GlSortingItem);
-
   afterEach(() => {
     wrapper.destroy();
-    wrapper = null;
   });
 
-  beforeEach(() => {
-    createComponent();
-  });
+  const findSorting = () => wrapper.findComponent(GlSorting);
+  const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
+  const findReleasedDateItem = () =>
+    findSortingItems().wrappers.find((item) => item.text() === 'Released date');
+  const findCreatedDateItem = () =>
+    findSortingItems().wrappers.find((item) => item.text() === 'Created date');
+  const getSortingItemsInfo = () =>
+    findSortingItems().wrappers.map((item) => ({
+      label: item.text(),
+      active: item.attributes().active === 'true',
+    }));
+
+  describe.each`
+    valueProp           | text               | isAscending | items
+    ${RELEASED_AT_ASC}  | ${'Released date'} | ${true}     | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+    ${RELEASED_AT_DESC} | ${'Released date'} | ${false}    | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+    ${CREATED_ASC}      | ${'Created date'}  | ${true}     | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+    ${CREATED_DESC}     | ${'Created date'}  | ${false}    | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+  `('component states', ({ valueProp, text, isAscending, items }) => {
+    beforeEach(() => {
+      createComponent(valueProp);
+    });
 
-  it('has all the sortable items', () => {
-    expect(findSortingItems()).toHaveLength(wrapper.vm.sortOptions.length);
+    it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => {
+      expect(findSorting().props()).toEqual(
+        expect.objectContaining({
+          text,
+          isAscending,
+        }),
+      );
+    });
+
+    it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => {
+      expect(getSortingItemsInfo()).toEqual(items);
+    });
   });
 
-  it('on sort change set sorting in vuex and emit event', () => {
-    findReleasesSorting().vm.$emit('sortDirectionChange');
-    expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { sort: 'asc' });
-    expect(wrapper.emitted('sort:changed')).toBeTruthy();
+  const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click');
+  const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click');
+  const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange');
+
+  const releasedAtDropdownItemDescription = 'released at dropdown item';
+  const createdAtDropdownItemDescription = 'created at dropdown item';
+  const sortDirectionButtonDescription = 'sort direction button';
+
+  describe.each`
+    initialValueProp    | itemClickFn                 | itemToClickDescription               | emittedEvent
+    ${RELEASED_AT_ASC}  | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${undefined}
+    ${RELEASED_AT_ASC}  | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${CREATED_ASC}
+    ${RELEASED_AT_ASC}  | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${RELEASED_AT_DESC}
+    ${RELEASED_AT_DESC} | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${undefined}
+    ${RELEASED_AT_DESC} | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${CREATED_DESC}
+    ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${RELEASED_AT_ASC}
+    ${CREATED_ASC}      | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC}
+    ${CREATED_ASC}      | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${undefined}
+    ${CREATED_ASC}      | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${CREATED_DESC}
+    ${CREATED_DESC}     | ${clickReleasedDateItem}    | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC}
+    ${CREATED_DESC}     | ${clickCreatedDateItem}     | ${createdAtDropdownItemDescription}  | ${undefined}
+    ${CREATED_DESC}     | ${clickSortDirectionButton} | ${sortDirectionButtonDescription}    | ${CREATED_ASC}
+  `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => {
+    beforeEach(() => {
+      createComponent(initialValueProp);
+      itemClickFn();
+    });
+
+    it(`emits ${
+      emittedEvent || 'nothing'
+    } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => {
+      expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent);
+    });
   });
 
-  it('on sort item click set sorting and emit event', () => {
-    const item = findSortingItems().at(0);
-    const { orderBy } = wrapper.vm.sortOptions[0];
-    item.vm.$emit('click');
-    expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { orderBy });
-    expect(wrapper.emitted('sort:changed')).toBeTruthy();
+  describe('prop validation', () => {
+    it('validates that the `value` prop is one of the expected sort strings', () => {
+      expect(() => {
+        createComponent('not a valid value');
+      }).toThrow('Invalid prop: custom validator check failed');
+    });
   });
 });
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
deleted file mode 100644
index 91406f7e2f4aff5dffb12ef8872916d371e60af4..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ /dev/null
@@ -1,197 +0,0 @@
-import { cloneDeep } from 'lodash';
-import originalGraphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import testAction from 'helpers/vuex_action_helper';
-import { PAGE_SIZE } from '~/releases/constants';
-import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
-import {
-  fetchReleases,
-  receiveReleasesError,
-  setSorting,
-} from '~/releases/stores/modules/index/actions';
-import * as types from '~/releases/stores/modules/index/mutation_types';
-import createState from '~/releases/stores/modules/index/state';
-import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-
-describe('Releases State actions', () => {
-  let mockedState;
-  let graphqlReleasesResponse;
-
-  const projectPath = 'root/test-project';
-  const projectId = 19;
-  const before = 'testBeforeCursor';
-  const after = 'testAfterCursor';
-
-  beforeEach(() => {
-    mockedState = {
-      ...createState({
-        projectId,
-        projectPath,
-      }),
-    };
-
-    graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
-  });
-
-  describe('fetchReleases', () => {
-    describe('GraphQL query variables', () => {
-      let vuexParams;
-
-      beforeEach(() => {
-        jest.spyOn(gqClient, 'query');
-
-        vuexParams = { dispatch: jest.fn(), commit: jest.fn(), state: mockedState };
-      });
-
-      describe('when neither a before nor an after parameter is provided', () => {
-        beforeEach(() => {
-          fetchReleases(vuexParams, { before: undefined, after: undefined });
-        });
-
-        it('makes a GraphQl query with a first variable', () => {
-          expect(gqClient.query).toHaveBeenCalledWith({
-            query: allReleasesQuery,
-            variables: { fullPath: projectPath, first: PAGE_SIZE, sort: 'RELEASED_AT_DESC' },
-          });
-        });
-      });
-
-      describe('when only a before parameter is provided', () => {
-        beforeEach(() => {
-          fetchReleases(vuexParams, { before, after: undefined });
-        });
-
-        it('makes a GraphQl query with last and before variables', () => {
-          expect(gqClient.query).toHaveBeenCalledWith({
-            query: allReleasesQuery,
-            variables: { fullPath: projectPath, last: PAGE_SIZE, before, sort: 'RELEASED_AT_DESC' },
-          });
-        });
-      });
-
-      describe('when only an after parameter is provided', () => {
-        beforeEach(() => {
-          fetchReleases(vuexParams, { before: undefined, after });
-        });
-
-        it('makes a GraphQl query with first and after variables', () => {
-          expect(gqClient.query).toHaveBeenCalledWith({
-            query: allReleasesQuery,
-            variables: { fullPath: projectPath, first: PAGE_SIZE, after, sort: 'RELEASED_AT_DESC' },
-          });
-        });
-      });
-
-      describe('when both before and after parameters are provided', () => {
-        it('throws an error', () => {
-          const callFetchReleases = () => {
-            fetchReleases(vuexParams, { before, after });
-          };
-
-          expect(callFetchReleases).toThrowError(
-            'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
-          );
-        });
-      });
-
-      describe('when the sort parameters are provided', () => {
-        it.each`
-          sort      | orderBy          | ReleaseSort
-          ${'asc'}  | ${'released_at'} | ${'RELEASED_AT_ASC'}
-          ${'desc'} | ${'released_at'} | ${'RELEASED_AT_DESC'}
-          ${'asc'}  | ${'created_at'}  | ${'CREATED_ASC'}
-          ${'desc'} | ${'created_at'}  | ${'CREATED_DESC'}
-        `(
-          'correctly sets $ReleaseSort based on $sort and $orderBy',
-          ({ sort, orderBy, ReleaseSort }) => {
-            mockedState.sorting.sort = sort;
-            mockedState.sorting.orderBy = orderBy;
-
-            fetchReleases(vuexParams, { before: undefined, after: undefined });
-
-            expect(gqClient.query).toHaveBeenCalledWith({
-              query: allReleasesQuery,
-              variables: { fullPath: projectPath, first: PAGE_SIZE, sort: ReleaseSort },
-            });
-          },
-        );
-      });
-    });
-
-    describe('when the request is successful', () => {
-      beforeEach(() => {
-        jest.spyOn(gqClient, 'query').mockResolvedValue(graphqlReleasesResponse);
-      });
-
-      it(`commits ${types.REQUEST_RELEASES} and ${types.RECEIVE_RELEASES_SUCCESS}`, () => {
-        const convertedResponse = convertAllReleasesGraphQLResponse(graphqlReleasesResponse);
-
-        return testAction(
-          fetchReleases,
-          {},
-          mockedState,
-          [
-            {
-              type: types.REQUEST_RELEASES,
-            },
-            {
-              type: types.RECEIVE_RELEASES_SUCCESS,
-              payload: {
-                data: convertedResponse.data,
-                pageInfo: convertedResponse.paginationInfo,
-              },
-            },
-          ],
-          [],
-        );
-      });
-    });
-
-    describe('when the request fails', () => {
-      beforeEach(() => {
-        jest.spyOn(gqClient, 'query').mockRejectedValue(new Error('Something went wrong!'));
-      });
-
-      it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => {
-        return testAction(
-          fetchReleases,
-          {},
-          mockedState,
-          [
-            {
-              type: types.REQUEST_RELEASES,
-            },
-          ],
-          [
-            {
-              type: 'receiveReleasesError',
-            },
-          ],
-        );
-      });
-    });
-  });
-
-  describe('receiveReleasesError', () => {
-    it('should commit RECEIVE_RELEASES_ERROR mutation', () => {
-      return testAction(
-        receiveReleasesError,
-        null,
-        mockedState,
-        [{ type: types.RECEIVE_RELEASES_ERROR }],
-        [],
-      );
-    });
-  });
-
-  describe('setSorting', () => {
-    it('should commit SET_SORTING', () => {
-      return testAction(
-        setSorting,
-        { orderBy: 'released_at', sort: 'asc' },
-        null,
-        [{ type: types.SET_SORTING, payload: { orderBy: 'released_at', sort: 'asc' } }],
-        [],
-      );
-    });
-  });
-});
diff --git a/spec/frontend/releases/stores/modules/list/helpers.js b/spec/frontend/releases/stores/modules/list/helpers.js
deleted file mode 100644
index 6669f44aa959383fd775e8c86c19ea5a47a65729..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/stores/modules/list/helpers.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import state from '~/releases/stores/modules/index/state';
-
-export const resetStore = (store) => {
-  store.replaceState(state());
-};
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
deleted file mode 100644
index 49e324c28a501b40cd9d92559be2b376b72c940a..0000000000000000000000000000000000000000
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import originalRelease from 'test_fixtures/api/releases/release.json';
-import graphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import * as types from '~/releases/stores/modules/index/mutation_types';
-import mutations from '~/releases/stores/modules/index/mutations';
-import createState from '~/releases/stores/modules/index/state';
-import { convertAllReleasesGraphQLResponse } from '~/releases/util';
-
-const originalReleases = [originalRelease];
-
-describe('Releases Store Mutations', () => {
-  let stateCopy;
-  let pageInfo;
-  let releases;
-
-  beforeEach(() => {
-    stateCopy = createState({});
-    pageInfo = convertAllReleasesGraphQLResponse(graphqlReleasesResponse).paginationInfo;
-    releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
-  });
-
-  describe('REQUEST_RELEASES', () => {
-    it('sets isLoading to true', () => {
-      mutations[types.REQUEST_RELEASES](stateCopy);
-
-      expect(stateCopy.isLoading).toEqual(true);
-    });
-  });
-
-  describe('RECEIVE_RELEASES_SUCCESS', () => {
-    beforeEach(() => {
-      mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
-        pageInfo,
-        data: releases,
-      });
-    });
-
-    it('sets is loading to false', () => {
-      expect(stateCopy.isLoading).toEqual(false);
-    });
-
-    it('sets hasError to false', () => {
-      expect(stateCopy.hasError).toEqual(false);
-    });
-
-    it('sets data', () => {
-      expect(stateCopy.releases).toEqual(releases);
-    });
-
-    it('sets pageInfo', () => {
-      expect(stateCopy.pageInfo).toEqual(pageInfo);
-    });
-  });
-
-  describe('RECEIVE_RELEASES_ERROR', () => {
-    it('resets data', () => {
-      mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
-        pageInfo,
-        data: releases,
-      });
-
-      mutations[types.RECEIVE_RELEASES_ERROR](stateCopy);
-
-      expect(stateCopy.isLoading).toEqual(false);
-      expect(stateCopy.releases).toEqual([]);
-      expect(stateCopy.pageInfo).toEqual({});
-    });
-  });
-
-  describe('SET_SORTING', () => {
-    it('should merge the sorting object with sort value', () => {
-      mutations[types.SET_SORTING](stateCopy, { sort: 'asc' });
-      expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, sort: 'asc' });
-    });
-
-    it('should merge the sorting object with order_by value', () => {
-      mutations[types.SET_SORTING](stateCopy, { orderBy: 'created_at' });
-      expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, orderBy: 'created_at' });
-    });
-  });
-});