diff --git a/.changeset/gentle-ligers-help.md b/.changeset/gentle-ligers-help.md
new file mode 100644
index 0000000000..f7f9c3b270
--- /dev/null
+++ b/.changeset/gentle-ligers-help.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after.
diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx
index df3d6f5a27..17d33693e5 100644
--- a/packages/core-app-api/src/app/AppManager.test.tsx
+++ b/packages/core-app-api/src/app/AppManager.test.tsx
@@ -34,6 +34,7 @@ import {
createSubRouteRef,
createRoutableExtension,
analyticsApiRef,
+ useApi,
} from '@backstage/core-plugin-api';
import { AppManager } from './AppManager';
import { AppComponents, AppIcons } from './types';
@@ -395,6 +396,44 @@ describe('Integration Test', () => {
});
});
+ it('feature flags should be available immediately', async () => {
+ const app = new AppManager({
+ apis: [
+ createApiFactory({
+ api: featureFlagsApiRef,
+ deps: { configApi: configApiRef },
+ factory() {
+ return new LocalStorageFeatureFlags();
+ },
+ }),
+ ],
+ defaultApis: [],
+ themes,
+ icons,
+ plugins: [createPlugin({ id: 'test', featureFlags: [{ name: 'foo' }] })],
+ components,
+ configLoader: async () => [],
+ });
+
+ const Provider = app.getProvider();
+ const Router = app.getRouter();
+
+ const FeatureFlags = () => {
+ const featureFlags = useApi(featureFlagsApiRef).getRegisteredFlags();
+ return
Flags: {featureFlags.map(f => f.name).join(',')}
;
+ };
+
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText('Flags: foo')).toBeInTheDocument();
+ });
+
it('should track route changes via analytics api', async () => {
const mockAnalyticsApi = new MockAnalyticsApi();
const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)];
diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx
index 4582a3c7cd..a6e15fe40c 100644
--- a/packages/core-app-api/src/app/AppManager.tsx
+++ b/packages/core-app-api/src/app/AppManager.tsx
@@ -21,8 +21,8 @@ import React, {
PropsWithChildren,
ReactElement,
useContext,
- useEffect,
useMemo,
+ useRef,
useState,
} from 'react';
import { Route, Routes } from 'react-router-dom';
@@ -230,6 +230,7 @@ export class AppManager implements BackstageApp {
let routesHaveBeenValidated = false;
const Provider = ({ children }: PropsWithChildren<{}>) => {
+ const needsFeatureFlagRegistrationRef = useRef(true);
const appThemeApi = useMemo(
() => AppThemeSelector.createWithStorage(this.themes),
[],
@@ -284,10 +285,21 @@ export class AppManager implements BackstageApp {
this.configApi = api;
}
- useEffect(() => {
- if (hasConfigApi) {
- const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!;
+ if ('node' in loadedConfig) {
+ // Loading or error
+ return loadedConfig.node;
+ }
+ // We can't register feature flags just after the element traversal, because the
+ // config API isn't available yet and implementations frequently depend on it.
+ // Instead we make it happen immediately, to make sure all flags are available
+ // for the first render.
+ if (hasConfigApi && needsFeatureFlagRegistrationRef.current) {
+ needsFeatureFlagRegistrationRef.current = false;
+
+ const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!;
+
+ if (featureFlagsApi) {
for (const plugin of this.plugins.values()) {
if ('getFeatureFlags' in plugin) {
for (const flag of plugin.getFeatureFlags()) {
@@ -314,11 +326,6 @@ export class AppManager implements BackstageApp {
featureFlagsApi.registerFlag({ name, pluginId: '' });
}
}
- }, [hasConfigApi, loadedConfig, featureFlags]);
-
- if ('node' in loadedConfig) {
- // Loading or error
- return loadedConfig.node;
}
const { ThemeProvider = AppThemeProvider } = this.components;