From 3ee8fc097349ecc160fe72cc9cc3777dc03f8714 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Fri, 24 Jul 2026 14:09:16 +0200
Subject: [PATCH] Add front component skeleton loader (#23261)
Front components (dashboard widget, side panel, settings preview) showed
blank space during their entire load. They now show a shimmering
full-area skeleton continuously, from the lazy chunk load through
metadata fetch, token/SDK wait, and worker boot, until the real UI
mounts.
The skeleton is threaded down as an optional `loadingFallback` prop so
the shared `twenty-front-component-renderer` package stays
dependency-free (react-loading-skeleton stays in twenty-front). The
command-menu headless component opts out by not passing a fallback, so
it stays blank as before.
---
.../components/FrontComponentRenderer.tsx | 10 +++++--
.../components/FrontComponentRenderer.tsx | 13 ++++++--
.../FrontComponentSkeletonLoader.tsx | 30 +++++++++++++++++++
.../FrontComponentWidgetRenderer.tsx | 4 ++-
.../SidePanelFrontComponentPage.tsx | 4 ++-
...ngsApplicationFrontComponentPreviewTab.tsx | 9 ++++--
6 files changed, 62 insertions(+), 8 deletions(-)
create mode 100644 packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx
diff --git a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
index 3585fa1bcb..f45cba7e97 100644
--- a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
+++ b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
@@ -11,7 +11,7 @@ import {
type RemoteReceiver,
RemoteRootRenderer,
} from '@remote-dom/react/host';
-import { useState } from 'react';
+import { type ReactNode, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { isDefined } from 'twenty-shared/utils';
@@ -35,6 +35,7 @@ type FrontComponentRendererProps = {
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
onError: (error?: Error) => void;
colorScheme: 'light' | 'dark';
+ loadingFallback?: ReactNode;
};
export const FrontComponentRenderer = ({
@@ -48,6 +49,7 @@ export const FrontComponentRenderer = ({
frontComponentHostCommunicationApi,
onError,
colorScheme,
+ loadingFallback,
}: FrontComponentRendererProps) => {
const [receiver, setReceiver] = useState(null);
const [thread, setThread] = useState(null);
@@ -55,6 +57,8 @@ export const FrontComponentRenderer = ({
const [isExecutionContextInitialized, setIsExecutionContextInitialized] =
useState(false);
+ const isReady = isDefined(receiver) && isExecutionContextInitialized;
+
return (
<>
)}
- {isDefined(receiver) && isExecutionContextInitialized && (
+ {!isDefined(error) && !isReady && loadingFallback}
+
+ {isReady && (
{
const { data, loading, error } = useQuery(FindOneFrontComponentDocument, {
variables: { id: frontComponentId },
@@ -55,11 +58,13 @@ export const FrontComponentRenderer = ({
return (
<>
+ {loading && loadingFallback}
{!loading && isDefined(frontComponent) && (
)}
>
@@ -70,6 +75,7 @@ const FrontComponentRendererContent = ({
frontComponent,
commandMenuItemId,
selectedRecordIds,
+ loadingFallback,
}: FrontComponentRendererContentProps) => {
const { colorScheme } = useContext(ThemeContext);
const { enqueueErrorSnackBar } = useSnackBar();
@@ -127,6 +133,7 @@ const FrontComponentRendererContent = ({
const applicationVariables = frontComponent.applicationVariables ?? undefined;
const isSdkClientReady = !usesSdkClient || !sdkClientChecksumsLoading;
+ const isReadyToRender = isDefined(applicationTokenPair) && isSdkClientReady;
return (
<>
@@ -134,7 +141,8 @@ const FrontComponentRendererContent = ({
frontComponentId={frontComponentId}
applicationTokenPair={applicationTokenPair}
/>
- {isDefined(applicationTokenPair) && isSdkClientReady && (
+ {!isReadyToRender && loadingFallback}
+ {isReadyToRender && (
)}
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx
new file mode 100644
index 0000000000..edc76970d2
--- /dev/null
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx
@@ -0,0 +1,30 @@
+import { styled } from '@linaria/react';
+import { useContext } from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { ThemeContext } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ height: 100%;
+ width: 100%;
+
+ & > span {
+ display: block;
+ height: 100%;
+ }
+`;
+
+export const FrontComponentSkeletonLoader = () => {
+ const { theme } = useContext(ThemeContext);
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
index 664db2ccbb..9b2c629f49 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
@@ -3,6 +3,7 @@ import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared/utils';
+import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
@@ -57,10 +58,11 @@ export const FrontComponentWidgetRenderer = ({
isCanvasLayout={layoutMode === PageLayoutTabLayoutMode.CANVAS}
isInEditMode={isPageLayoutInEditMode}
>
-
+ }>
}
/>
diff --git a/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx
index 142ba5934b..19d22d7aca 100644
--- a/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx
+++ b/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx
@@ -1,5 +1,6 @@
import { Suspense, lazy } from 'react';
+import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader';
import { viewableFrontComponentIdComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentIdComponentState';
import { viewableFrontComponentRecordContextComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -31,10 +32,11 @@ export const SidePanelFrontComponentPage = () => {
: undefined;
return (
-
+ }>
}
/>
);
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx
index 8cf18ae9e8..95897b1859 100644
--- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx
@@ -4,6 +4,8 @@ import { Suspense, lazy } from 'react';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader';
+
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
@@ -64,8 +66,11 @@ export const SettingsApplicationFrontComponentPreviewTab = ({
) : (
-
-
+ }>
+ }
+ />
)}