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;