diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 0b8f5ffa397ca7cb3e5e91c215bbc3b44e8e8607..51acd4b9d9b072b95bda32e04ded3ff6891a5801 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -5,9 +5,15 @@ import ExtensionBase from './base.vue';
 // Holds all the currently registered extensions
 export const registeredExtensions = Vue.observable({ extensions: [] });
 
+const createCustomOptionsWithFallback = (extension) => (options) => {
+  return options.reduce((acc, option) => {
+    acc[option] = extension[option] ?? ExtensionBase[option];
+    return acc;
+  }, {});
+};
+
 export const registerExtension = (extension) => {
-  // Pushes into the extenions array a dynamically created Vue component
-  // that gets exteneded from `base.vue`
+  const customOptions = createCustomOptionsWithFallback(extension);
   registeredExtensions.extensions.push(
     markRaw({
       extends: ExtensionBase,
@@ -18,12 +24,16 @@ export const registerExtension = (extension) => {
           required: true,
         },
       },
-      telemetry: extension.telemetry,
-      i18n: extension.i18n,
-      expandEvent: extension.expandEvent,
-      enablePolling: extension.enablePolling,
-      enableExpandedPolling: extension.enableExpandedPolling,
-      modalComponent: extension.modalComponent,
+      // Vue 3 doesn't copy custom component options with Vue.extend
+      // We have to explicitly fallback to the base component if an option is missing
+      ...customOptions([
+        'telemetry',
+        'i18n',
+        'expandEvent',
+        'enablePolling',
+        'enableExpandedPolling',
+        'modalComponent',
+      ]),
       computed: {
         ...extension.props.reduce(
           (acc, propKey) => ({
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
index 976226328fcb83c70c26d7248807a625421963a9..cf9c4cce13ecefdabd3d0665ce339352011b6068 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -162,7 +162,7 @@ export default {
           <rect x="32" y="2" width="302" height="20" rx="4" />
         </gl-skeleton-loader>
       </template>
-      <template v-else>
+      <template v-if="!isLoading" #default>
         <bold-text :message="summaryText" />
       </template>
     </state-container>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 2c7cb273eba4105c89ebc49727afb5302070c0a3..421e7842825f7b0d734cfdbf0cd71eed4878d37d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -296,7 +296,7 @@ export default {
       );
   },
   beforeDestroy() {
-    eventHub.$off('mr.discussion.updated', this.checkStatus);
+    this.unbindEventListeners();
 
     if (this.deploymentsInterval) {
       this.deploymentsInterval.destroy();
@@ -469,48 +469,47 @@ export default {
     stopPolling() {
       this.$apollo.queries.state.stopPolling();
     },
+    checkRebasedStatus(cb) {
+      this.checkStatus(cb, true);
+    },
+    setIsRemovingSourceBranch([value]) {
+      this.mr.isRemovingSourceBranch = value;
+    },
+    setMergeError(mergeError) {
+      this.mr.state = 'failedToMerge';
+      this.mr.mergeError = mergeError;
+    },
+    setMrData(data) {
+      this.mr.setData(data);
+    },
+    onFetchDeployments() {
+      this.fetchPreMergeDeployments();
+      if (this.shouldRenderMergedPipeline) {
+        this.fetchPostMergeDeployments();
+      }
+    },
     bindEventHubListeners() {
-      eventHub.$on('MRWidgetUpdateRequested', (cb) => {
-        this.checkStatus(cb);
-      });
-
-      eventHub.$on('MRWidgetRebaseSuccess', (cb) => {
-        this.checkStatus(cb, true);
-      });
-
-      // `params` should be an Array contains a Boolean, like `[true]`
-      // Passing parameter as Boolean didn't work.
-      eventHub.$on('SetBranchRemoveFlag', (params) => {
-        [this.mr.isRemovingSourceBranch] = params;
-      });
-
-      eventHub.$on('FailedToMerge', (mergeError) => {
-        this.mr.state = 'failedToMerge';
-        this.mr.mergeError = mergeError;
-      });
-
-      eventHub.$on('UpdateWidgetData', (data) => {
-        this.mr.setData(data);
-      });
-
-      eventHub.$on('FetchActionsContent', () => {
-        this.fetchActionsContent();
-      });
-
-      eventHub.$on('EnablePolling', () => {
-        this.resumePolling();
-      });
-
-      eventHub.$on('DisablePolling', () => {
-        this.stopPolling();
-      });
-
-      eventHub.$on('FetchDeployments', () => {
-        this.fetchPreMergeDeployments();
-        if (this.shouldRenderMergedPipeline) {
-          this.fetchPostMergeDeployments();
-        }
-      });
+      eventHub.$on('MRWidgetUpdateRequested', this.checkStatus);
+      eventHub.$on('MRWidgetRebaseSuccess', this.checkRebasedStatus);
+      eventHub.$on('SetBranchRemoveFlag', this.setIsRemovingSourceBranch);
+      eventHub.$on('FailedToMerge', this.setMergeError);
+      eventHub.$on('UpdateWidgetData', this.setMrData);
+      eventHub.$on('FetchActionsContent', this.fetchActionsContent);
+      eventHub.$on('EnablePolling', this.resumePolling);
+      eventHub.$on('DisablePolling', this.stopPolling);
+      eventHub.$on('FetchDeployments', this.onFetchDeployments);
+    },
+    unbindEventListeners() {
+      eventHub.$off('MRWidgetUpdateRequested', this.checkStatus);
+      eventHub.$off('MRWidgetRebaseSuccess', this.checkRebasedStatus);
+      eventHub.$off('SetBranchRemoveFlag', this.setIsRemovingSourceBranch);
+      eventHub.$off('FailedToMerge', this.setMergeError);
+      eventHub.$off('UpdateWidgetData', this.setMrData);
+      eventHub.$off('FetchActionsContent', this.fetchActionsContent);
+      eventHub.$off('EnablePolling', this.resumePolling);
+      eventHub.$off('DisablePolling', this.stopPolling);
+      eventHub.$off('FetchDeployments', this.onFetchDeployments);
+      eventHub.$off('mr.discussion.updated', this.checkStatus);
     },
     dismissSuggestPipelines() {
       this.mr.isDismissedSuggestPipeline = true;
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 44454ea8a477e2b0e4ca70fc1eb97e1b7f9b6c0b..0f30c850020c544b5425d2131dcb1fd7dfa26133 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -43,6 +43,9 @@ import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/appro
 import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
 import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
 import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
+import ExtensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+
 import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
 import mockData, { mockDeployment, mockMergePipeline, mockPostMergeDeployments } from './mock_data';
 import {
@@ -75,7 +78,6 @@ describe('MrWidgetOptions', () => {
   let queryResponse;
   let wrapper;
   let mock;
-  let stateSubscription;
 
   const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
 
@@ -83,6 +85,9 @@ describe('MrWidgetOptions', () => {
     updatedMrData = {},
     options = {},
     data = {},
+    stateSubscriptionHandler = jest
+      .fn()
+      .mockResolvedValue({ data: { mergeRequestMergeStatusUpdated: {} } }),
     mountFn = shallowMountExtended,
   } = {}) => {
     gl.mrWidgetData = { ...mockData, ...updatedMrData };
@@ -103,7 +108,6 @@ describe('MrWidgetOptions', () => {
       },
     };
     stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
-    stateSubscription = createMockApolloSubscription();
 
     const queryHandlers = [
       [approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
@@ -119,13 +123,11 @@ describe('MrWidgetOptions', () => {
         conflictsStateQuery,
         jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }),
       ],
-      ...(options.apolloMock || []),
     ];
     const subscriptionHandlers = [
       [approvedBySubscription, () => mockedApprovalsSubscription],
-      [getStateSubscription, () => stateSubscription],
+      [getStateSubscription, stateSubscriptionHandler],
       [readyToMergeSubscription, () => createMockApolloSubscription()],
-      ...(options.apolloSubscriptions || []),
     ];
     const apolloProvider = createMockApollo(queryHandlers);
 
@@ -170,9 +172,6 @@ describe('MrWidgetOptions', () => {
   });
 
   afterEach(() => {
-    mock.restore();
-    // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
-    wrapper.destroy();
     gl.mrWidgetData = {};
   });
 
@@ -358,6 +357,7 @@ describe('MrWidgetOptions', () => {
               mr: {
                 setData: mockSetData,
                 setGraphqlData: jest.fn(),
+                setGraphqlSubscriptionData: jest.fn(),
               },
             },
           });
@@ -421,30 +421,27 @@ describe('MrWidgetOptions', () => {
       });
 
       describe('bindEventHubListeners', () => {
-        const mockSetData = jest.fn();
+        let mockSetData;
+
         beforeEach(async () => {
-          await createComponent({
-            data: {
-              mr: {
-                setData: mockSetData,
-                setGraphqlData: jest.fn(),
-              },
-            },
-          });
+          mockSetData = jest.spyOn(MRWidgetStore.prototype, 'setData');
+          await createComponent();
         });
 
         it('refetches when "MRWidgetUpdateRequested" event is emitted', async () => {
           expect(stateQueryHandler).toHaveBeenCalledTimes(1);
           eventHub.$emit('MRWidgetUpdateRequested', () => {});
           await waitForPromises();
-          expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+          // apollo query resolves twice with notifyOnNetworkStatusChange enabled, so 3 calls in total
+          expect(stateQueryHandler).toHaveBeenCalledTimes(3);
         });
 
         it('refetches when "MRWidgetRebaseSuccess" event is emitted', async () => {
           expect(stateQueryHandler).toHaveBeenCalledTimes(1);
           eventHub.$emit('MRWidgetRebaseSuccess', () => {});
           await waitForPromises();
-          expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+          // apollo query resolves twice with notifyOnNetworkStatusChange enabled, so 3 calls in total
+          expect(stateQueryHandler).toHaveBeenCalledTimes(3);
         });
 
         it('should bind to SetBranchRemoveFlag', () => {
@@ -459,10 +456,10 @@ describe('MrWidgetOptions', () => {
 
         it('should bind to FailedToMerge', async () => {
           expect(findAlertMessage().exists()).toBe(false);
-          expect(findPipelineContainer().props('mr')).toMatchObject({
-            mergeError: undefined,
-            state: 'merged',
-          });
+          const props = findPipelineContainer().props('mr');
+          expect(props.state).toBe('merged');
+          // Due to Vue 2 and 3 differences in handling props we must check for both undefined and null
+          expect(props.mergeError == null).toBe(true);
           const mergeError = 'Something bad happened!';
           await eventHub.$emit('FailedToMerge', mergeError);
 
@@ -567,6 +564,7 @@ describe('MrWidgetOptions', () => {
               mr: {
                 setData: mockSetData,
                 setGraphqlData: mockSetGraphqlData,
+                setGraphqlSubscriptionData: jest.fn(),
               },
               service: {
                 checkStatus: mockCheckStatus,
@@ -748,7 +746,7 @@ describe('MrWidgetOptions', () => {
           .exists(),
       ).toBe(false);
 
-      await nextTick();
+      await waitForPromises();
 
       const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
       expect(collapsedSection.exists()).toBe(true);
@@ -952,11 +950,14 @@ describe('MrWidgetOptions', () => {
         extension = workingExtension();
       });
 
-      it('reports events without a CE suffix', () => {
+      it('reports events without a CE suffix', async () => {
         extension.name = `${extension.name}CE`;
 
         registerExtension(extension);
-        createComponent({ mountFn: mountExtended });
+        await createComponent({
+          mountFn: mountExtended,
+          options: { stubs: { ExtensionsContainer } },
+        });
 
         expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
           'i_code_review_merge_request_widget_test_extension_view',
@@ -966,11 +967,14 @@ describe('MrWidgetOptions', () => {
         );
       });
 
-      it('reports events without a EE suffix', () => {
+      it('reports events without a EE suffix', async () => {
         extension.name = `${extension.name}EE`;
 
         registerExtension(extension);
-        createComponent({ mountFn: mountExtended });
+        await createComponent({
+          mountFn: mountExtended,
+          options: { stubs: { ExtensionsContainer } },
+        });
 
         expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
           'i_code_review_merge_request_widget_test_extension_view',
@@ -980,11 +984,14 @@ describe('MrWidgetOptions', () => {
         );
       });
 
-      it('leaves non-CE & non-EE all caps suffixes intact', () => {
+      it('leaves non-CE & non-EE all caps suffixes intact', async () => {
         extension.name = `${extension.name}HI`;
 
         registerExtension(extension);
-        createComponent({ mountFn: mountExtended });
+        await createComponent({
+          mountFn: mountExtended,
+          options: { stubs: { ExtensionsContainer } },
+        });
 
         expect(api.trackRedisHllUserEvent).not.toHaveBeenCalledWith(
           'i_code_review_merge_request_widget_test_extension_view',
@@ -994,11 +1001,14 @@ describe('MrWidgetOptions', () => {
         );
       });
 
-      it("doesn't remove CE or EE from the middle of a widget name", () => {
+      it("doesn't remove CE or EE from the middle of a widget name", async () => {
         extension.name = 'TestCEExtensionEETest';
 
         registerExtension(extension);
-        createComponent({ mountFn: mountExtended });
+        await createComponent({
+          mountFn: mountExtended,
+          options: { stubs: { ExtensionsContainer } },
+        });
 
         expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
           'i_code_review_merge_request_widget_test_c_e_extension_e_e_test_view',
@@ -1006,9 +1016,12 @@ describe('MrWidgetOptions', () => {
       });
     });
 
-    it('triggers view events when mounted', () => {
+    it('triggers view events when mounted', async () => {
       registerExtension(workingExtension());
-      createComponent({ mountFn: mountExtended });
+      await createComponent({
+        mountFn: mountExtended,
+        options: { stubs: { ExtensionsContainer } },
+      });
 
       expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
       expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
@@ -1050,9 +1063,13 @@ describe('MrWidgetOptions', () => {
       });
     });
 
-    it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
+    it('triggers the "full report clicked" events when the appropriate button is clicked', async () => {
       registerExtension(fullReportExtension);
-      createComponent({ mountFn: mountExtended });
+
+      await createComponent({
+        mountFn: mountExtended,
+        options: { stubs: { ExtensionsContainer } },
+      });
 
       api.trackRedisHllUserEvent.mockClear();
       api.trackRedisCounterEvent.mockClear();
@@ -1135,10 +1152,13 @@ describe('MrWidgetOptions', () => {
       });
 
       it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
+        const stateSubscription = createMockApolloSubscription();
+
         await createComponent({
           updatedMrData: { state: 'opened', detailedMergeStatus: 'PREPARING' },
           options: {},
           data: {},
+          stateSubscriptionHandler: () => stateSubscription,
         });
 
         expect(wrapper.html()).toContain('mr-widget-preparing-stub');