diff --git a/.changeset/neat-mails-teach.md b/.changeset/neat-mails-teach.md
new file mode 100644
index 0000000000..162cbfef93
--- /dev/null
+++ b/.changeset/neat-mails-teach.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-app-api': minor
+---
+
+Add `loadingComponent` parameter to `createApp()`
diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md
index 3236b05a01..0375a67482 100644
--- a/packages/frontend-app-api/api-report.md
+++ b/packages/frontend-app-api/api-report.md
@@ -8,6 +8,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { FrontendFeature } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
+import { ReactNode } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SubRouteRef } from '@backstage/frontend-plugin-api';
@@ -21,6 +22,7 @@ export function createApp(options?: {
config: ConfigApi;
}>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
+ loadingComponent?: ReactNode;
}): {
createRoot(): JSX_2.Element;
};
diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx
index 70f8cfa2ae..94254a2e01 100644
--- a/packages/frontend-app-api/src/wiring/createApp.test.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx
@@ -297,4 +297,36 @@ describe('createApp', () => {
"
`);
});
+
+ it('should use "Loading..." as the default suspense fallback', async () => {
+ const app = createApp({
+ configLoader: () => new Promise(() => {}),
+ });
+
+ await renderWithEffects(app.createRoot());
+
+ await expect(screen.findByText('Loading...')).resolves.toBeInTheDocument();
+ });
+
+ it('should use no suspense fallback if the "loadingComponent" is null', async () => {
+ const app = createApp({
+ configLoader: () => new Promise(() => {}),
+ loadingComponent: null,
+ });
+
+ await renderWithEffects(app.createRoot());
+
+ expect(screen.queryByText('Loading...')).toBeNull();
+ });
+
+ it('should use a custom "loadingComponent"', async () => {
+ const app = createApp({
+ configLoader: () => new Promise(() => {}),
+ loadingComponent: "Custom loading message",
+ });
+
+ await renderWithEffects(app.createRoot());
+
+ expect(screen.queryByText('Custom loading message')).toBeNull();
+ });
});
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index 591e1cbf25..6e0a2c7cd7 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import React, { JSX } from 'react';
+import React, { JSX, ReactNode } from 'react';
import { ConfigReader } from '@backstage/config';
import {
AppTree,
@@ -170,9 +170,21 @@ export function createApp(options?: {
features?: (FrontendFeature | CreateAppFeatureLoader)[];
configLoader?: () => Promise<{ config: ConfigApi }>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
+ /**
+ * The component to render while loading the app (waiting for config, features, etc)
+ *
+ * Is the text "Loading..." by default.
+ * If set to "null" then no loading fallback component is rendered. *
+ */
+ loadingComponent?: ReactNode;
}): {
createRoot(): JSX.Element;
} {
+ let suspenseFallback = options?.loadingComponent;
+ if (suspenseFallback === undefined) {
+ suspenseFallback = 'Loading...';
+ }
+
async function appLoader() {
const config =
(await options?.configLoader?.().then(c => c.config)) ??
@@ -214,7 +226,7 @@ export function createApp(options?: {
createRoot() {
const LazyApp = React.lazy(appLoader);
return (
-
+
);