Merge pull request #33152 from backstage/rugvip/use-api-holder-no-throw

frontend-plugin-api: change `useApiHolder` to not throw outside API context
This commit is contained in:
Patrik Oldsberg
2026-03-05 14:10:02 +01:00
committed by GitHub
4 changed files with 47 additions and 8 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/core-plugin-api': patch
---
Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context.
@@ -114,12 +114,16 @@ describe('ApiProvider', () => {
withLogCollector(['error'], () => {
expect(() => {
render(<MyHookConsumer />);
}).toThrow(/^API context is not available/);
}).toThrow('No implementation available for apiRef{x}');
}).error,
).toEqual([
expect.stringContaining('Error: API context is not available'),
expect.stringContaining(
'Error: No implementation available for apiRef{x}',
),
expect.objectContaining({ type: 'unhandled-exception' }),
expect.stringContaining('Error: API context is not available'),
expect.stringContaining(
'Error: No implementation available for apiRef{x}',
),
expect.objectContaining({ type: 'unhandled-exception' }),
expect.stringContaining(
'The above error occurred in the <MyHookConsumer> component',
@@ -130,12 +134,16 @@ describe('ApiProvider', () => {
withLogCollector(['error'], () => {
expect(() => {
render(<MyHocConsumer />);
}).toThrow(/^API context is not available/);
}).toThrow('No implementation available for apiRef{x}');
}).error,
).toEqual([
expect.stringContaining('Error: API context is not available'),
expect.stringContaining(
'Error: No implementation available for apiRef{x}',
),
expect.objectContaining({ type: 'unhandled-exception' }),
expect.stringContaining('Error: API context is not available'),
expect.stringContaining(
'Error: No implementation available for apiRef{x}',
),
expect.objectContaining({ type: 'unhandled-exception' }),
expect.stringContaining(
'The above error occurred in the <withApis(Component)> component',
@@ -17,7 +17,30 @@
import { renderHook } from '@testing-library/react';
import { createVersionedContextForTesting } from '@backstage/version-bridge';
import { createApiRef } from './ApiRef';
import { useApi } from './useApi';
import { useApi, useApiHolder } from './useApi';
describe('useApiHolder', () => {
const context = createVersionedContextForTesting('api-context');
afterEach(() => {
context.reset();
});
it('should return the API holder from context', () => {
const holder = { get: jest.fn() };
context.set({ 1: holder });
const renderedHook = renderHook(() => useApiHolder());
expect(renderedHook.result.current).toBe(holder);
});
it('should return an empty API holder when there is no context', () => {
const renderedHook = renderHook(() => useApiHolder());
const holder = renderedHook.result.current;
expect(holder.get(createApiRef<string>({ id: 'x' }))).toBeUndefined();
});
});
describe('useApi', () => {
const context = createVersionedContextForTesting('api-context');
@@ -19,6 +19,8 @@ import { ApiRef, ApiHolder, TypesToApiRefs } from './types';
import { useVersionedContext } from '@backstage/version-bridge';
import { NotImplementedError } from '@backstage/errors';
const emptyApiHolder: ApiHolder = Object.freeze({ get: () => undefined });
/**
* React hook for retrieving {@link ApiHolder}, an API catalog.
*
@@ -27,7 +29,7 @@ import { NotImplementedError } from '@backstage/errors';
export function useApiHolder(): ApiHolder {
const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context');
if (!versionedHolder) {
throw new NotImplementedError('API context is not available');
return emptyApiHolder;
}
const apiHolder = versionedHolder.atVersion(1);