Fix front component worker re-creation on every render (#19245)

- `frontComponentHostCommunicationApi` gets a new object reference on
every render, causing the `useMemo`/`useEffect` in
`FrontComponentWorkerEffect` to tear down and re-create the web worker
each time.
- Decouple the host API lifecycle from the worker lifecycle by moving
thread.exports updates into a dedicated
`FrontComponentUpdateHostCommunicationApiEffect` that mutates the
thread's exports object in place via Object.assign.
- Rename `FrontComponentHostCommunicationApiEffect` to
`FrontComponentInitializeHostCommunicationApiEffect` for clarity.

## Before


https://github.com/user-attachments/assets/6f3a5c14-2ae7-4317-82b5-1625abb4143e

## After


https://github.com/user-attachments/assets/1059d6cd-e02c-4477-b3e8-8e965a716434
This commit is contained in:
Raphaël Bosi
2026-04-02 11:29:42 +02:00
committed by GitHub
parent 2c73d47555
commit 16033e9f99
5 changed files with 50 additions and 13 deletions
@@ -1,6 +1,7 @@
import { FrontComponentErrorEffect } from '@/remote/components/FrontComponentErrorEffect';
import { FrontComponentHostCommunicationApiEffect } from '@/remote/components/FrontComponentHostCommunicationApiEffect';
import { FrontComponentInitializeHostCommunicationApiEffect } from '@/remote/components/FrontComponentInitializeHostCommunicationApiEffect';
import { FrontComponentUpdateContextEffect } from '@/remote/components/FrontComponentUpdateContextEffect';
import { FrontComponentUpdateHostCommunicationApiEffect } from '@/remote/components/FrontComponentUpdateHostCommunicationApiEffect';
import { type FrontComponentHostCommunicationApi } from '@/types/FrontComponentHostCommunicationApi';
import { type SdkClientUrls } from '@/types/HostToWorkerRenderContext';
import { type WorkerExports } from '@/types/WorkerExports';
@@ -55,7 +56,6 @@ export const FrontComponentRenderer = ({
apiUrl={apiUrl}
sdkClientUrls={sdkClientUrls}
frontComponentId={executionContext.frontComponentId}
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
setReceiver={setReceiver}
setThread={setThread}
setError={setError}
@@ -63,7 +63,6 @@ export const FrontComponentRenderer = ({
);
}, [
componentUrl,
frontComponentHostCommunicationApi,
setError,
setReceiver,
setThread,
@@ -102,7 +101,13 @@ export const FrontComponentRenderer = ({
{isDefined(thread) && (
<>
<FrontComponentHostCommunicationApiEffect thread={thread} />
<FrontComponentUpdateHostCommunicationApiEffect
thread={thread}
frontComponentHostCommunicationApi={
frontComponentHostCommunicationApi
}
/>
<FrontComponentInitializeHostCommunicationApiEffect thread={thread} />
<FrontComponentUpdateContextEffect
thread={thread}
executionContext={executionContext}
@@ -1,8 +1,9 @@
export { FrontComponentRenderer } from './host/components/FrontComponentRenderer';
export { componentRegistry } from './host/generated/host-component-registry';
export { FrontComponentErrorEffect } from './remote/components/FrontComponentErrorEffect';
export { FrontComponentHostCommunicationApiEffect } from './remote/components/FrontComponentHostCommunicationApiEffect';
export { FrontComponentInitializeHostCommunicationApiEffect } from './remote/components/FrontComponentInitializeHostCommunicationApiEffect';
export { FrontComponentUpdateContextEffect } from './remote/components/FrontComponentUpdateContextEffect';
export { FrontComponentUpdateHostCommunicationApiEffect } from './remote/components/FrontComponentUpdateHostCommunicationApiEffect';
export { FrontComponentWorkerEffect } from './remote/components/FrontComponentWorkerEffect';
export {
HtmlA,
@@ -3,13 +3,13 @@ import { type WorkerExports } from '@/types/WorkerExports';
import { type ThreadWebWorker } from '@quilted/threads';
import { useEffect } from 'react';
type FrontComponentHostCommunicationApiEffectProps = {
type FrontComponentInitializeHostCommunicationApiEffectProps = {
thread: ThreadWebWorker<WorkerExports, FrontComponentHostCommunicationApi>;
};
export const FrontComponentHostCommunicationApiEffect = ({
export const FrontComponentInitializeHostCommunicationApiEffect = ({
thread,
}: FrontComponentHostCommunicationApiEffectProps) => {
}: FrontComponentInitializeHostCommunicationApiEffectProps) => {
useEffect(() => {
thread.imports.initializeHostCommunicationApi().catch((error) => {
console.error('Failed to initialize host communication API:', error);
@@ -0,0 +1,20 @@
import { type FrontComponentHostCommunicationApi } from '@/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/types/WorkerExports';
import { type ThreadWebWorker } from '@quilted/threads';
import { useEffect } from 'react';
type FrontComponentUpdateHostCommunicationApiEffectProps = {
thread: ThreadWebWorker<WorkerExports, FrontComponentHostCommunicationApi>;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
};
export const FrontComponentUpdateHostCommunicationApiEffect = ({
thread,
frontComponentHostCommunicationApi,
}: FrontComponentUpdateHostCommunicationApiEffectProps) => {
useEffect(() => {
Object.assign(thread.exports, frontComponentHostCommunicationApi);
}, [thread, frontComponentHostCommunicationApi]);
return null;
};
@@ -1,8 +1,8 @@
import { ThreadWebWorker, release, retain } from '@quilted/threads';
import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
import { type ConfirmationModalCaller } from 'twenty-shared/types';
import { type CommandConfirmationModalResult } from 'twenty-sdk';
import { type ConfirmationModalCaller } from 'twenty-shared/types';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type SdkClientUrls } from '../../types/HostToWorkerRenderContext';
import { type WorkerExports } from '../../types/WorkerExports';
@@ -17,13 +17,26 @@ type CommandMenuItemConfirmationModalResultBrowserEventDetail = {
confirmationResult: CommandConfirmationModalResult;
};
const noopAsync = async () => {};
const HOST_COMMUNICATION_API_NOOP_INITIALIZATION: FrontComponentHostCommunicationApi =
{
navigate: noopAsync,
requestAccessTokenRefresh: async () => '',
openSidePanelPage: noopAsync,
openCommandConfirmationModal: noopAsync,
unmountFrontComponent: noopAsync,
enqueueSnackbar: noopAsync,
closeSidePanel: noopAsync,
updateProgress: noopAsync,
};
type FrontComponentWorkerEffectProps = {
componentUrl: string;
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
frontComponentId: string;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
setThread: React.Dispatch<
React.SetStateAction<ThreadWebWorker<
@@ -40,7 +53,6 @@ export const FrontComponentWorkerEffect = ({
apiUrl,
sdkClientUrls,
frontComponentId,
frontComponentHostCommunicationApi,
setReceiver,
setThread,
setError,
@@ -68,7 +80,7 @@ export const FrontComponentWorkerEffect = ({
WorkerExports,
FrontComponentHostCommunicationApi
>(worker, {
exports: frontComponentHostCommunicationApi,
exports: { ...HOST_COMMUNICATION_API_NOOP_INITIALIZATION },
});
const handleCommandMenuItemConfirmationModalResultBrowserEvent = (
@@ -135,7 +147,6 @@ export const FrontComponentWorkerEffect = ({
setError,
setReceiver,
setThread,
frontComponentHostCommunicationApi,
]);
return null;