diff --git a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
index 4d70ada109b16..bbf092b32d565 100644
--- a/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
+++ b/web/packages/teleterm/src/ui/AccessRequestCheckout/useAccessRequestCheckout.ts
@@ -33,6 +33,7 @@ import { useSpecifiableFields } from 'shared/components/AccessRequests/NewReques
import { CreateRequest } from 'shared/components/AccessRequests/Shared/types';
import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
import {
PendingAccessRequest,
extractResourceRequestProperties,
@@ -54,7 +55,7 @@ import { makeUiAccessRequest } from '../DocumentAccessRequests/useAccessRequests
export default function useAccessRequestCheckout() {
const ctx = useAppContext();
- ctx.workspacesService.useState();
+ useWorkspaceServiceState();
ctx.clustersService.useState();
const clusterUri =
ctx.workspacesService?.getActiveWorkspace()?.localClusterUri;
diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx b/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx
index 9bfe8e87e19c1..47f5e9efb4c69 100644
--- a/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx
+++ b/web/packages/teleterm/src/ui/ConnectMyComputer/connectMyComputerContext.test.tsx
@@ -18,13 +18,11 @@
import { EventEmitter } from 'node:events';
-import React from 'react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { makeErrorAttempt } from 'shared/hooks/useAsync';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
-import { WorkspaceContextProvider } from 'teleterm/ui/Documents';
import { AgentProcessState } from 'teleterm/mainProcess/types';
import * as resourcesContext from 'teleterm/ui/DocumentCluster/resourcesContext';
import {
@@ -90,13 +88,11 @@ function renderUseConnectMyComputerContextHook(
return renderHook(() => useConnectMyComputerContext(), {
wrapper: ({ children }) => (
-
-
-
- {children}
-
-
-
+
+
+ {children}
+
+
),
});
@@ -322,15 +318,13 @@ describe('canUse', () => {
const { result } = renderHook(() => useConnectMyComputerContext(), {
wrapper: ({ children }) => (
-
-
-
- {children}
-
-
-
+
+
+ {children}
+
+
),
});
diff --git a/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx b/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx
index f3e89ee30c022..949ef3e96cefc 100644
--- a/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx
+++ b/web/packages/teleterm/src/ui/Documents/workspaceContext.tsx
@@ -16,21 +16,27 @@
* along with this program. If not, see .
*/
-import React, { PropsWithChildren } from 'react';
+import {
+ FC,
+ PropsWithChildren,
+ useCallback,
+ useContext,
+ createContext,
+} from 'react';
import { DocumentsService } from 'teleterm/ui/services/workspacesService';
import { AccessRequestsService } from 'teleterm/ui/services/workspacesService/accessRequestsService';
-import { useAppContext } from 'teleterm/ui/appContextProvider';
import { ClusterUri, RootClusterUri } from 'teleterm/ui/uri';
+import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector';
-const WorkspaceContext = React.createContext<{
+const WorkspaceContext = createContext<{
rootClusterUri: RootClusterUri;
localClusterUri: ClusterUri;
documentsService: DocumentsService;
accessRequestsService: AccessRequestsService;
}>(null);
-export const WorkspaceContextProvider: React.FC<
+export const WorkspaceContextProvider: FC<
PropsWithChildren<{
value: {
rootClusterUri: RootClusterUri;
@@ -40,12 +46,29 @@ export const WorkspaceContextProvider: React.FC<
};
}>
> = props => {
+ // Re-render the context provider whenever the state of the relevant workspace changes. The
+ // context provider cannot re-render only when its props change.
+ // For example, if a new document gets added, none of the props are going to change, but the
+ // callsite that uses useWorkspaceContext might want to get re-rendered in this case, as
+ // technically documentsService returned from useWorkspaceContext might return new state.
+ useStoreSelector(
+ 'workspacesService',
+ useCallback(
+ state => state.workspaces[props.value.rootClusterUri],
+ [props.value.rootClusterUri]
+ )
+ );
return ;
};
export const useWorkspaceContext = () => {
- const ctx = useAppContext();
- ctx.workspacesService.useState();
+ const context = useContext(WorkspaceContext);
- return React.useContext(WorkspaceContext);
+ if (!context) {
+ throw new Error(
+ 'useWorkspaceContext must be used within a WorkspaceContextProvider'
+ );
+ }
+
+ return context;
};
diff --git a/web/packages/teleterm/src/ui/Search/SearchBar.tsx b/web/packages/teleterm/src/ui/Search/SearchBar.tsx
index 87f5c36264651..a25ef17f75aa6 100644
--- a/web/packages/teleterm/src/ui/Search/SearchBar.tsx
+++ b/web/packages/teleterm/src/ui/Search/SearchBar.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useRef, useEffect } from 'react';
+import React, { useRef, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { Box, Flex } from 'design';
@@ -31,14 +31,17 @@ import {
} from 'teleterm/ui/services/keyboardShortcuts';
import { useAppContext } from '../appContextProvider';
+import { useStoreSelector } from '../hooks/useStoreSelector';
const OPEN_SEARCH_BAR_SHORTCUT_ACTION: KeyboardShortcutAction = 'openSearchBar';
export function SearchBarConnected() {
- const { workspacesService } = useAppContext();
- workspacesService.useState();
+ const rootClusterUri = useStoreSelector(
+ 'workspacesService',
+ useCallback(state => state.rootClusterUri, [])
+ );
- if (!workspacesService.getRootClusterUri()) {
+ if (!rootClusterUri) {
return null;
}
diff --git a/web/packages/teleterm/src/ui/Search/SearchContext.tsx b/web/packages/teleterm/src/ui/Search/SearchContext.tsx
index 86682321d3ef8..49bdea9fe1df2 100644
--- a/web/packages/teleterm/src/ui/Search/SearchContext.tsx
+++ b/web/packages/teleterm/src/ui/Search/SearchContext.tsx
@@ -33,6 +33,7 @@ import { useAppContext } from 'teleterm/ui/appContextProvider';
import {
Document,
DocumentClusterQueryParams,
+ useWorkspaceServiceState,
} from 'teleterm/ui/services/workspacesService';
import { actionPicker, SearchPicker } from './pickers/pickers';
@@ -130,7 +131,7 @@ export const SearchContextProvider: FC = props => {
);
}
- appContext.workspacesService.useState();
+ useWorkspaceServiceState();
const activeDocument = appContext.workspacesService
.getActiveWorkspaceDocumentService()
?.getActive();
diff --git a/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts b/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts
index 84a53651f75e1..8e98ad977f6ec 100644
--- a/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts
+++ b/web/packages/teleterm/src/ui/Search/pickers/useDisplayResults.ts
@@ -25,13 +25,14 @@ import {
DisplayResults,
} from 'teleterm/ui/Search/searchResult';
import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
export function useDisplayResults(args: {
filters: SearchFilter[];
inputValue: string;
}): DisplayResults {
const { workspacesService } = useAppContext();
- workspacesService.useState();
+ useWorkspaceServiceState();
const localClusterUri =
workspacesService.getActiveWorkspace()?.localClusterUri;
diff --git a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx
index 460126b2d0869..6e88d8c1ebba0 100644
--- a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx
+++ b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/ShareFeedback.test.tsx
@@ -16,14 +16,17 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { fireEvent, render } from 'design/utils/testing';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { IAppContext } from 'teleterm/ui/types';
-import { Cluster } from 'teleterm/services/tshd/types';
+
+import {
+ makeLoggedInUser,
+ makeRootCluster,
+} from 'teleterm/services/tshd/testHelpers';
import { ShareFeedback } from './ShareFeedback';
@@ -41,48 +44,42 @@ function renderOpenedShareFeedback(appContext: IAppContext) {
test('email field is not prefilled with the username if is not an email', () => {
const appContext = new MockAppContext();
const clusterUri = '/clusters/localhost';
- jest
- .spyOn(appContext.clustersService, 'findCluster')
- .mockImplementation(() => {
- return {
- loggedInUser: { name: 'alice' },
- } as Cluster;
- });
-
- jest
- .spyOn(appContext.workspacesService, 'getRootClusterUri')
- .mockReturnValue(clusterUri);
+ appContext.workspacesService.setState(draft => {
+ draft.rootClusterUri = clusterUri;
+ });
+ appContext.clustersService.setState(draft => {
+ draft.clusters.set(
+ clusterUri,
+ makeRootCluster({
+ uri: clusterUri,
+ loggedInUser: makeLoggedInUser({ name: 'alice' }),
+ })
+ );
+ });
renderOpenedShareFeedback(appContext);
- expect(appContext.clustersService.findCluster).toHaveBeenCalledWith(
- clusterUri
- );
expect(screen.getByLabelText('Email Address')).toHaveValue('');
});
test('email field is prefilled with the username if it looks like an email', () => {
const appContext = new MockAppContext();
const clusterUri = '/clusters/production';
- jest
- .spyOn(appContext.clustersService, 'findCluster')
- .mockImplementation(() => {
- return {
- loggedInUser: {
- name: 'bob@prod.com',
- },
- } as Cluster;
- });
-
- jest
- .spyOn(appContext.workspacesService, 'getRootClusterUri')
- .mockReturnValue(clusterUri);
+ appContext.workspacesService.setState(draft => {
+ draft.rootClusterUri = clusterUri;
+ });
+ appContext.clustersService.setState(draft => {
+ draft.clusters.set(
+ clusterUri,
+ makeRootCluster({
+ uri: clusterUri,
+ loggedInUser: makeLoggedInUser({ name: 'bob@prod.com' }),
+ })
+ );
+ });
renderOpenedShareFeedback(appContext);
- expect(appContext.clustersService.findCluster).toHaveBeenCalledWith(
- clusterUri
- );
expect(screen.getByLabelText('Email Address')).toHaveValue('bob@prod.com');
});
diff --git a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts
index 2c3d695fc1b22..a180c47b2fa41 100644
--- a/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts
+++ b/web/packages/teleterm/src/ui/StatusBar/ShareFeedback/useShareFeedback.ts
@@ -16,13 +16,14 @@
* along with this program. If not, see .
*/
-import { useState } from 'react';
+import { useCallback, useState } from 'react';
import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
import { staticConfig } from 'teleterm/staticConfig';
import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector';
import { ShareFeedbackFormValues } from './types';
@@ -30,7 +31,10 @@ export const FEEDBACK_TOO_LONG_ERROR = 'FEEDBACK_TOO_LONG_ERROR';
export function useShareFeedback() {
const ctx = useAppContext();
- ctx.workspacesService.useState();
+ const rootClusterUri = useStoreSelector(
+ 'workspacesService',
+ useCallback(state => state.rootClusterUri, [])
+ );
ctx.clustersService.useState();
const [isShareFeedbackOpened, setIsShareFeedbackOpened] = useState(false);
@@ -74,9 +78,7 @@ export function useShareFeedback() {
}
function getEmailFromUserName(): string {
- const cluster = ctx.clustersService.findCluster(
- ctx.workspacesService.getRootClusterUri()
- );
+ const cluster = ctx.clustersService.findCluster(rootClusterUri);
const userName = cluster?.loggedInUser?.name;
if (/^\S+@\S+$/.test(userName)) {
return userName;
diff --git a/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts b/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts
index 093ea470db3f7..486ee85c57856 100644
--- a/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts
+++ b/web/packages/teleterm/src/ui/StatusBar/useAccessRequestCheckoutButton.ts
@@ -16,11 +16,13 @@
* along with this program. If not, see .
*/
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
+
import { useAppContext } from '../appContextProvider';
export function useAccessRequestsButton() {
const ctx = useAppContext();
- ctx.workspacesService.useState();
+ useWorkspaceServiceState();
const workspaceAccessRequest =
ctx.workspacesService.getActiveWorkspaceAccessRequestsService();
diff --git a/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts b/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts
index c9650be3d94c5..bd785cf51d72a 100644
--- a/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts
+++ b/web/packages/teleterm/src/ui/StatusBar/useActiveDocumentClusterBreadcrumbs.ts
@@ -17,12 +17,15 @@
*/
import { useAppContext } from 'teleterm/ui/appContextProvider';
-import { getResourceUri } from 'teleterm/ui/services/workspacesService';
+import {
+ getResourceUri,
+ useWorkspaceServiceState,
+} from 'teleterm/ui/services/workspacesService';
import { routing } from 'teleterm/ui/uri';
export function useActiveDocumentClusterBreadcrumbs(): string {
const ctx = useAppContext();
- ctx.workspacesService.useState();
+ useWorkspaceServiceState();
ctx.clustersService.useState();
const activeDocument = ctx.workspacesService
diff --git a/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx b/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx
index 3e3a6a392a69d..7410532aff073 100644
--- a/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx
+++ b/web/packages/teleterm/src/ui/TabHost/TabHost.test.tsx
@@ -18,7 +18,7 @@
import 'jest-canvas-mock';
import { createRef } from 'react';
-import { fireEvent, render, screen } from 'design/utils/testing';
+import { fireEvent, render, screen, act } from 'design/utils/testing';
import { TabHost } from 'teleterm/ui/TabHost/TabHost';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
@@ -26,7 +26,11 @@ import { Document } from 'teleterm/ui/services/workspacesService';
import { TabContextMenuOptions } from 'teleterm/mainProcess/types';
import { makeDocumentCluster } from 'teleterm/ui/services/workspacesService/documentsService/testHelpers';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
-import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
+import {
+ makeRootCluster,
+ rootClusterUri,
+} from 'teleterm/services/tshd/testHelpers';
+import { routing } from 'teleterm/ui/uri';
function getMockDocuments(): Document[] {
return [
@@ -43,8 +47,6 @@ function getMockDocuments(): Document[] {
];
}
-const rootClusterUri = '/clusters/test_uri';
-
async function getTestSetup({ documents }: { documents: Document[] }) {
const appContext = new MockAppContext();
jest.spyOn(appContext.mainProcessClient, 'openTabContextMenu');
@@ -64,7 +66,10 @@ async function getTestSetup({ documents }: { documents: Document[] }) {
documents,
location: documents[0]?.uri,
localClusterUri: rootClusterUri,
- accessRequests: undefined,
+ accessRequests: {
+ isBarCollapsed: true,
+ pending: { kind: 'resource', resources: new Map() },
+ },
};
});
@@ -137,16 +142,24 @@ test('open context menu', async () => {
const options: TabContextMenuOptions = openTabContextMenu.mock.calls[0][0];
expect(options.document).toEqual(document);
- options.onClose();
+ act(() => {
+ options.onClose();
+ });
expect(close).toHaveBeenCalledWith(document.uri);
- options.onCloseOthers();
+ act(() => {
+ options.onCloseOthers();
+ });
expect(closeOthers).toHaveBeenCalledWith(document.uri);
- options.onCloseToRight();
+ act(() => {
+ options.onCloseToRight();
+ });
expect(closeToRight).toHaveBeenCalledWith(document.uri);
- options.onDuplicatePty();
+ act(() => {
+ options.onDuplicatePty();
+ });
expect(duplicatePtyAndActivate).toHaveBeenCalledWith(document.uri);
});
@@ -155,7 +168,15 @@ test('open new tab', async () => {
documents: [getMockDocuments()[0]],
});
const { add, open } = docsService;
- const mockedClusterDocument = makeDocumentCluster();
+ // Use a URI of a cluster that's not in ClustersService so that DocumentCluster doesn't render
+ // UnifiedResources for it. UnifiedResources requires a lot of mocks to be set up.
+ const nonExistentClusterUri = routing.getClusterUri({
+ ...routing.parseClusterUri(rootClusterUri).params,
+ leafClusterId: 'nonexistent-leaf',
+ });
+ const mockedClusterDocument = makeDocumentCluster({
+ clusterUri: nonExistentClusterUri,
+ });
docsService.createClusterDocument = () => mockedClusterDocument;
const $newTabButton = screen.getByTitle('New Tab', { exact: false });
diff --git a/web/packages/teleterm/src/ui/TabHost/TabHost.tsx b/web/packages/teleterm/src/ui/TabHost/TabHost.tsx
index 8e59d1ef01668..504c53583e143 100644
--- a/web/packages/teleterm/src/ui/TabHost/TabHost.tsx
+++ b/web/packages/teleterm/src/ui/TabHost/TabHost.tsx
@@ -16,11 +16,12 @@
* along with this program. If not, see .
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import styled from 'styled-components';
import { Flex } from 'design';
import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
import * as types from 'teleterm/ui/services/workspacesService/documentsService/types';
import { canDocChangeShell } from 'teleterm/ui/services/workspacesService/documentsService/types';
import { Tabs } from 'teleterm/ui/Tabs';
@@ -29,6 +30,8 @@ import { IAppContext } from 'teleterm/ui/types';
import { useKeyboardShortcutFormatters } from 'teleterm/ui/services/keyboardShortcuts';
import { Shell } from 'teleterm/mainProcess/shell';
+import { useStoreSelector } from '../hooks/useStoreSelector';
+
import { useTabShortcuts } from './useTabShortcuts';
import { useNewTabOpener } from './useNewTabOpener';
import { ClusterConnectPanel } from './ClusterConnectPanel/ClusterConnectPanel';
@@ -37,8 +40,10 @@ export function TabHostContainer(props: {
topBarContainerRef: React.MutableRefObject;
}) {
const ctx = useAppContext();
- ctx.workspacesService.useState();
- const isRootClusterSelected = !!ctx.workspacesService.getRootClusterUri();
+ const isRootClusterSelected = useStoreSelector(
+ 'workspacesService',
+ useCallback(state => !!state.rootClusterUri, [])
+ );
if (isRootClusterSelected) {
return ;
@@ -53,6 +58,7 @@ export function TabHost({
ctx: IAppContext;
topBarContainerRef: React.MutableRefObject;
}) {
+ useWorkspaceServiceState();
const documentsService =
ctx.workspacesService.getActiveWorkspaceDocumentService();
const activeDocument = documentsService?.getActive();
diff --git a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
index 493c7f2828cfe..17f59846bc01a 100644
--- a/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
+++ b/web/packages/teleterm/src/ui/TopBar/AdditionalActions.tsx
@@ -32,6 +32,7 @@ import { KeyboardShortcutAction } from 'teleterm/services/config';
import { useKeyboardShortcutFormatters } from 'teleterm/ui/services/keyboardShortcuts';
import { ListItem } from 'teleterm/ui/components/ListItem';
import { useNewTabOpener } from 'teleterm/ui/TabHost';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
type MenuItem = {
title: string;
@@ -48,7 +49,7 @@ type MenuItemConditionallyDisabled = { isDisabled: true; disabledText: string };
function useMenuItems(): MenuItem[] {
const ctx = useAppContext();
const { workspacesService, mainProcessClient, notificationsService } = ctx;
- workspacesService.useState();
+ useWorkspaceServiceState();
ctx.clustersService.useState();
const documentsService =
workspacesService.getActiveWorkspaceDocumentService();
diff --git a/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts b/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts
index a51ddcb05bf1f..fb40c0aff790e 100644
--- a/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts
+++ b/web/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts
@@ -17,13 +17,14 @@
*/
import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
import { ClusterUri } from 'teleterm/ui/uri';
export function useClusters() {
const { workspacesService, clustersService, commandLauncher } =
useAppContext();
- workspacesService.useState();
+ useWorkspaceServiceState();
clustersService.useState();
function findLeaves(clusterUri: string) {
diff --git a/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts b/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts
index 2df389a4205a9..581061db169aa 100644
--- a/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts
+++ b/web/packages/teleterm/src/ui/TopBar/Identity/useIdentity.ts
@@ -19,12 +19,13 @@
import { useAppContext } from 'teleterm/ui/appContextProvider';
import { Cluster, LoggedInUser } from 'teleterm/services/tshd/types';
import { RootClusterUri } from 'teleterm/ui/uri';
+import { useWorkspaceServiceState } from 'teleterm/ui/services/workspacesService';
export function useIdentity() {
const ctx = useAppContext();
ctx.clustersService.useState();
- ctx.workspacesService.useState();
+ useWorkspaceServiceState();
async function changeRootCluster(clusterUri: RootClusterUri): Promise {
await ctx.workspacesService.setActiveWorkspace(clusterUri);
diff --git a/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts b/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts
index 42bc74a989bb6..42ee066e6b702 100644
--- a/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts
+++ b/web/packages/teleterm/src/ui/hooks/useLoggedInUser.ts
@@ -16,10 +16,14 @@
* along with this program. If not, see .
*/
+import { useCallback } from 'react';
+
import { useAppContext } from 'teleterm/ui/appContextProvider';
import { useWorkspaceContext } from 'teleterm/ui/Documents';
import { LoggedInUser } from 'teleterm/services/tshd/types';
+import { useStoreSelector } from './useStoreSelector';
+
/**
* useLoggedInUser returns the user logged into the root cluster of the active workspace. The return
* value changes depending on the active workspace.
@@ -30,11 +34,14 @@ import { LoggedInUser } from 'teleterm/services/tshd/types';
* It might return undefined if there's no active workspace.
*/
export function useLoggedInUser(): LoggedInUser | undefined {
- const { clustersService, workspacesService } = useAppContext();
+ const { clustersService } = useAppContext();
clustersService.useState();
- workspacesService.useState();
- const clusterUri = workspacesService.getRootClusterUri();
+ const clusterUri = useStoreSelector(
+ 'workspacesService',
+ useCallback(store => store.rootClusterUri, [])
+ );
+
if (!clusterUri) {
return;
}
diff --git a/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts b/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts
index f0f2f07cbece2..16c1f133c5520 100644
--- a/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts
+++ b/web/packages/teleterm/src/ui/hooks/useStoreSelector.ts
@@ -70,3 +70,15 @@ export const useStoreSelector = <
type ImmutableStoreKeys = {
[K in keyof T]: T[K] extends ImmutableStore ? K : never;
}[keyof T];
+
+/**
+ * identitySelector returns the whole state of the given store.
+ *
+ * Useful during refactorings of legacy code which depends on the useStore which triggers a
+ * re-render on any change to the store.
+ *
+ * Should be used sparingly. It's often a better idea to make the selector as narrow as possible.
+ */
+export function identitySelector(state: Value): Value {
+ return state;
+}
diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
index d4288ac72dff5..d2899b02df939 100644
--- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
+++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts
@@ -17,7 +17,6 @@
*/
import { z } from 'zod';
-import { useStore } from 'shared/libs/stores';
import { arrayObjectIsEqual } from 'shared/utils/highbar';
import {
@@ -44,6 +43,11 @@ import {
routing,
} from 'teleterm/ui/uri';
+import {
+ identitySelector,
+ useStoreSelector,
+} from 'teleterm/ui/hooks/useStoreSelector';
+
import {
AccessRequestsService,
getEmptyPendingAccessRequest,
@@ -220,10 +224,6 @@ export class WorkspacesService extends ImmutableStore {
);
}
- useState() {
- return useStore(this);
- }
-
setState(nextState: (draftState: WorkspacesState) => WorkspacesState | void) {
super.setState(nextState);
this.persistState();
@@ -616,3 +616,15 @@ const unifiedResourcePreferencesSchema = z
type UnifiedResourcePreferencesSchemaAsRequired = Required<
z.infer
>;
+
+/**
+ * useWorkspaceServiceState is a replacement for the legacy useStore hook. Many components within
+ * teleterm depend on the behavior of useStore which re-renders the component on any change within
+ * the store. Most of the time, those components don't even use the state returned by useStore.
+ *
+ * @deprecated Prefer useStoreSelector with a selector that picks only what the callsite is going
+ * to use. useWorkspaceServiceState re-renders the component on any change within any workspace.
+ */
+export const useWorkspaceServiceState = () => {
+ return useStoreSelector('workspacesService', identitySelector);
+};