Files
twenty/packages/twenty-sdk/src/front-component-renderer/remote/components/FrontComponentWorkerEffect.tsx
T
Raphaël Bosi 7da8450075 [FRONT COMPONENTS] Headless components (#18096)
## Description

- Add `isHeadless` field to `FrontComponent` entity so front components
can run without rendering UI in the command menu
- Introduce headless front component mounting logic:
`HeadlessFrontComponentMountRoot` at the application root,
`useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent`
hooks to mount/unmount headless components
- Expand the SDK with new action components (`Action`, `ActionLink`,
`ActionOpenSidePanelPage`) and host communication functions
(`openSidePanelPage`, `unmountFrontComponent`)
- Move `CommandMenuPages` type to twenty-shared so the SDK can reference
it for side panel navigation

## Video QA



https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
2026-02-20 14:14:42 +00:00

89 lines
2.3 KiB
TypeScript

import { ThreadWebWorker, release, retain } from '@quilted/threads';
import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '../../types/WorkerExports';
import { createRemoteWorker } from '../worker/utils/createRemoteWorker';
type FrontComponentWorkerEffectProps = {
componentUrl: string;
applicationAccessToken?: string;
apiUrl?: string;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
setThread: React.Dispatch<
React.SetStateAction<ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
> | null>
>;
setError: React.Dispatch<React.SetStateAction<Error | null>>;
};
export const FrontComponentWorkerEffect = ({
componentUrl,
applicationAccessToken,
apiUrl,
frontComponentHostCommunicationApi,
setReceiver,
setThread,
setError,
}: FrontComponentWorkerEffectProps) => {
const isInitializedRef = useRef(false);
useEffect(() => {
if (isInitializedRef.current) {
return;
}
const newReceiver = new RemoteReceiver({ retain, release });
const worker = createRemoteWorker();
worker.onerror = (event: ErrorEvent) => {
const workerError =
event.error ?? new Error(event.message || 'Unknown worker error');
console.error('[FrontComponentRenderer] Worker error:', workerError);
setError(workerError);
};
const thread = new ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
>(worker, {
exports: frontComponentHostCommunicationApi,
});
setThread(thread);
thread.imports
.render(newReceiver.connection, {
componentUrl,
applicationAccessToken,
apiUrl,
})
.catch((error: Error) => {
setError(error);
});
setReceiver(newReceiver);
isInitializedRef.current = true;
return () => {
setThread(null);
worker.terminate();
};
}, [
componentUrl,
applicationAccessToken,
apiUrl,
setError,
setReceiver,
setThread,
frontComponentHostCommunicationApi,
]);
return null;
};