diff --git a/.changeset/use-api-holder-no-throw.md b/.changeset/use-api-holder-no-throw.md new file mode 100644 index 0000000000..465cc9f401 --- /dev/null +++ b/.changeset/use-api-holder-no-throw.md @@ -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. diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index 4273587c00..0a899ff88a 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -114,12 +114,16 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).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 component', @@ -130,12 +134,16 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).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 component', diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx index 4b9d80fb62..7596105810 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx @@ -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({ id: 'x' }))).toBeUndefined(); + }); +}); describe('useApi', () => { const context = createVersionedContextForTesting('api-context'); diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx index 5a8f5d5dab..2808f13f1f 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -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);