Add loadingComponent parameter to createApp()

This allows Backstage instances to show their own
"fallback" content when the app is loading, such as to
avoid the "flicker of white" on-reload.

Signed-off-by: Mitchell Hentges <mhentges@spotify.com>
This commit is contained in:
Mitchell Hentges
2024-03-13 11:42:20 +01:00
parent 739415b07c
commit 48d6628af9
4 changed files with 53 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
Add `loadingComponent` parameter to `createApp()`
+2
View File
@@ -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;
};
@@ -297,4 +297,36 @@ describe('createApp', () => {
</app>"
`);
});
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: <span>"Custom loading message"</span>,
});
await renderWithEffects(app.createRoot());
expect(screen.queryByText('Custom loading message')).toBeNull();
});
});
@@ -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 (
<React.Suspense fallback="Loading...">
<React.Suspense fallback={suspenseFallback}>
<LazyApp />
</React.Suspense>
);