Repository: opensearch-project/OpenSearch-Dashboards (TypeScript)

Issue:
[BUG] ScopedHistory instance has fell out of scope for basePath: `/app/data-explorer`
**Describe the bug**

When we switch the query language in discover page, the console outputs the below error. On top of that, the action of navigating from discover page to another page will output a series of the following error as well. 

It seems like the scope history in data explorer needs to be better handled. 

```
scoped_history.ts:252 Uncaught (in promise) Error: ScopedHistory instance has fell out of navigation scope for basePath: /app/data-explorer
    at ScopedHistory.verifyActive (scoped_history.ts:252:1)
    at get location [as location] (scoped_history.ts:207:1)
    at getCurrentUrl (parse.ts:37:1)
    at getPendingUrl (osd_url_storage.ts:146:1)
    at flush (osd_url_storage.ts:135:1)
    at osd_url_storage.ts:163:1
```

**To Reproduce**
Steps to reproduce the behavior:
1. Go to discover page
2. Click on the toggle on query language selector on the top navigation bar. 
3. See error




Discussion: @joshuarrrr Can I try this one?
@ashwin-pc here is the screenshot recording, i started the dashboard in Incognito:

https://github.com/opensearch-project/OpenSearch-Dashboards/assets/43937633/7e7486f2-5980-488c-9c1f-c6b4a82fe570


Hi,
is this related to my issue that the browser back button is not working like it did prior to 2.10 when the new discover tool got introduced?



Fix (patch):
diff --git a/changelogs/fragments/9167.yml b/changelogs/fragments/9167.yml
new file mode 100644
index 000000000000..028d20a5be56
--- /dev/null
+++ b/changelogs/fragments/9167.yml
@@ -0,0 +1,2 @@
+fix:
+- Cleanup OsdUrlStateStorage subscription in TopNav ([#9167](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9167))
\ No newline at end of file
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index b120c69af694..620bbd12f042 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -465,6 +465,7 @@ export { Filter, Query, RefreshInterval, TimeRange } from '../common';
 export {
   createSavedQueryService,
   connectStorageToQueryState,
+  useConnectStorageToQueryState,
   connectToQueryState,
   syncQueryStateWithUrl,
   QueryState,
diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts
index 66e212bd6468..7efd77095d81 100644
--- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts
+++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts
@@ -31,7 +31,6 @@
 import { Subscription } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
 import _ from 'lodash';
-import { CoreStart } from 'opensearch-dashboards/public';
 import {
   BaseStateContainer,
   IOsdUrlStateStorage,
@@ -41,27 +40,28 @@ import { QueryState, QueryStateChange } from './types';
 import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common';
 import { validateTimeRange } from '../timefilter';
 
+export interface ISyncConfig {
+  filters: FilterStateStore;
+  query: boolean;
+  dataset?: boolean;
+}
+
 /**
  * Helper function to sync up filter and query services in data plugin
  * with a URL state storage so plugins can persist the app filter and query
  * values across refresh
- * @param QueryService: either setup or start
- * @param  OsdUrlStateStorage to use for syncing and store data
+ * @param queryService: either setup or start
+ * @param osdUrlStateStorage to use for syncing and store data
  * @param syncConfig app filter and query
  */
-export const connectStorageToQueryState = async (
+export const connectStorageToQueryState = (
   {
     filterManager,
     queryString,
     state$,
   }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>,
-  OsdUrlStateStorage: IOsdUrlStateStorage,
-  syncConfig: {
-    filters: FilterStateStore;
-    query: boolean;
-    dataSet?: boolean;
-  },
-  uiSettings?: CoreStart['uiSettings']
+  osdUrlStateStorage: IOsdUrlStateStorage,
+  syncConfig: ISyncConfig
 ) => {
   try {
     const syncKeys: Array<keyof QueryStateChange> = [];
@@ -72,14 +72,14 @@ export const connectStorageToQueryState = async (
       syncKeys.push('appFilters');
     }
 
-    const initialStateFromURL: QueryState = OsdUrlStateStorage.get('_q') ?? {
+    const initialStateFromURL: QueryState = osdUrlStateStorage.get('_q') ?? {
       query: queryString.getDefaultQuery(),
       filters: filterManager.getAppFilters(),
     };
 
     // set up initial '_q' flag in the URL to sync query and filter changes
-    if (!OsdUrlStateStorage.get('_q')) {
-      OsdUrlStateStorage.set('_q', initialStateFromURL, {
+    if (!osdUrlStateStorage.get('_q')) {
+      osdUrlStateStorage.set('_q', initialStateFromURL, {
         replace: true,
       });
     }
@@ -126,7 +126,7 @@ export const connectStorageToQueryState = async (
             })
           )
           .subscribe((newState) => {
-            OsdUrlStateStorage.set('_q', newState, {
+            osdUrlStateStorage.set('_q', newState, {
               replace: true,
             });
           }),
diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts
index 5c5619461e08..1a80c36d0d67 100644
--- a/src/plugins/data/public/query/state_sync/index.ts
+++ b/src/plugins/data/public/query/state_sync/index.ts
@@ -29,5 +29,6 @@
  */
 
 export { connectToQueryState, connectStorageToQueryState } from './connect_to_query_state';
+export { useConnectStorageToQueryState } from './use_connect_to_query_state';
 export { syncQueryStateWithUrl } from './sync_state_with_url';
 export { QueryState, QueryStateChange } from './types';
diff --git a/src/plugins/data/public/query/state_sync/use_connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/use_connect_to_query_state.ts
new file mode 100644
index 000000000000..336764e72433
--- /dev/null
+++ b/src/plugins/data/public/query/state_sync/use_connect_to_query_state.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useEffect } from 'react';
+import { QuerySetup, QueryStart } from '../query_service';
+import { IOsdUrlStateStorage } from '../../../../opensearch_dashboards_utils/public';
+import { connectStorageToQueryState, ISyncConfig } from './connect_to_query_state';
+
+/**
+ * Hook version of connectStorageToQueryState, automatically unSub
+ * from OsdUrlStateStorage
+ * @param queryService: either setup or start
+ * @param osdUrlStateStorage to use for syncing and store data
+ * @param syncConfig app filter and query
+ */
+export const useConnectStorageToQueryState = (
+  queryService: Pick<
+    QueryStart | QuerySetup,
+    'timefilter' | 'filterManager' | 'queryString' | 'state$'
+  >,
+  osdUrlStateStorage: IOsdUrlStateStorage,
+  syncConfig: ISyncConfig
+) => {
+  useEffect(() => {
+    const unSub = connectStorageToQueryState(queryService, osdUrlStateStorage, syncConfig);
+    return () => {
+      if (unSub) {
+        unSub();
+      }
+    };
+  }, [osdUrlStateStorage, queryService, syncConfig]);
+};
diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx
index 6f827e563ec6..cb407c0ca0df 100644
--- a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx
+++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx
@@ -10,7 +10,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/e
 import { i18n } from '@osd/i18n';
 import { AppMountParameters } from '../../../../../../core/public';
 import {
-  connectStorageToQueryState,
+  useConnectStorageToQueryState,
   opensearchFilters,
   QueryStatus,
 } from '../../../../../data/public';
@@ -65,15 +65,14 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro
     ? getTopNavLinks(services, inspectorAdapters, savedSearch, isEnhancementsEnabled)
     : [];
 
-  connectStorageToQueryState(
-    services.data.query,
-    osdUrlStateStorage,
-    {
+  const syncConfig = useMemo(() => {
+    return {
       filters: opensearchFilters.FilterStateStore.APP_STATE,
       query: true,
-    },
-    uiSettings
-  );
+    };
+  }, []);
+
+  useConnectStorageToQueryState(services.data.query, osdUrlStateStorage, syncConfig);
 
   useEffect(() => {
     const subscription = data$.subscribe((queryData) => {
diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx
index b88755cd9962..6d0235d07f5b 100644
--- a/src/plugins/vis_builder/public/application/components/top_nav.tsx
+++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useState, useRef, useMemo } from 'react';
 import { isEqual } from 'lodash';
 import { useParams } from 'react-router-dom';
 import { useUnmount } from 'react-use';
@@ -20,7 +20,7 @@ import { setEditorState } from '../utils/state_management/metadata_slice';
 import { useCanSave } from '../utils/use/use_can_save';
 import { saveStateToSavedObject } from '../../saved_visualizations/transforms';
 import { TopNavMenuData, TopNavMenuItemRenderType } from '../../../../navigation/public';
-import { opensearchFilters, connectStorageToQueryState } from '../../../../data/public';
+import { opensearchFilters, useConnectStorageToQueryState } from '../../../../data/public';
 import { RootState } from '../../../../data_explorer/public';
 
 function useDeepEffect(callback, dependencies) {
@@ -56,16 +56,21 @@ export const TopNav = () => {
 
   const saveDisabledReason = useCanSave();
   const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl);
-  connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, {
-    filters: opensearchFilters.FilterStateStore.APP_STATE,
-    query: true,
-  });
   const { selected: indexPattern } = useIndexPatterns();
   const [config, setConfig] = useState<TopNavMenuData[] | undefined>();
   const originatingApp = useTypedSelector((state) => {
     return state.metadata.originatingApp;
   });
 
+  const syncConfig = useMemo(() => {
+    return {
+      filters: opensearchFilters.FilterStateStore.APP_STATE,
+      query: true,
+    };
+  }, []);
+
+  useConnectStorageToQueryState(services.data.query, services.osdUrlStateStorage, syncConfig);
+
   useEffect(() => {
     const getConfig = () => {
       if (!savedVisBuilderVis || !indexPattern) return;