diff --git a/packages/jsapi-bootstrap/src/DeferredApiBootstrap.test.tsx b/packages/jsapi-bootstrap/src/DeferredApiBootstrap.test.tsx
new file mode 100644
index 0000000000..275a695b54
--- /dev/null
+++ b/packages/jsapi-bootstrap/src/DeferredApiBootstrap.test.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { act, render } from '@testing-library/react';
+import type { dh as DhType } from '@deephaven/jsapi-types';
+import { TestUtils } from '@deephaven/utils';
+import DeferredApiBootstrap from './DeferredApiBootstrap';
+import { DeferredApiContext } from './useDeferredApi';
+
+it('should call the error callback if no API provider wrapped', () => {
+ const onError = jest.fn();
+ render();
+ expect(onError).toHaveBeenCalled();
+});
+
+it('renders children if the API is loaded', () => {
+ const api = TestUtils.createMockProxy();
+ const { queryByText } = render(
+
+
+ Child
+
+
+ );
+ expect(queryByText('Child')).not.toBeNull();
+});
+
+it('waits to render children until the API is loaded', async () => {
+ let resolveApi: (api: DhType) => void;
+ const apiPromise = new Promise(resolve => {
+ resolveApi = resolve;
+ });
+ const deferredApi = jest.fn(() => apiPromise);
+ const options = { foo: 'bar' };
+ const { queryByText } = render(
+
+
+ Child
+
+
+ );
+ expect(queryByText('Child')).toBeNull();
+ expect(deferredApi).toHaveBeenCalledTimes(1);
+ expect(deferredApi).toHaveBeenCalledWith(options);
+
+ const api = TestUtils.createMockProxy();
+ await act(async () => {
+ resolveApi(api);
+ await apiPromise;
+ });
+ expect(queryByText('Child')).not.toBeNull();
+});
diff --git a/packages/jsapi-bootstrap/src/DeferredApiBootstrap.tsx b/packages/jsapi-bootstrap/src/DeferredApiBootstrap.tsx
new file mode 100644
index 0000000000..f5ec339cfd
--- /dev/null
+++ b/packages/jsapi-bootstrap/src/DeferredApiBootstrap.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import useDeferredApi from './useDeferredApi';
+import { ApiContext } from './ApiBootstrap';
+
+type DeferredApiBootstrapProps = React.PropsWithChildren<{
+ onError?: (error: unknown) => void;
+ /**
+ * Options to use when fetching the deferred API.
+ */
+ options?: Record;
+}>;
+
+/**
+ * Does not render children until the deferred API is resolved.
+ */
+export const DeferredApiBootstrap = React.memo(
+ ({
+ children,
+ onError,
+ options,
+ }: DeferredApiBootstrapProps): JSX.Element | null => {
+ const [api, apiError] = useDeferredApi(options);
+ if (apiError != null) {
+ onError?.(apiError);
+ return null;
+ }
+ if (api == null) {
+ // Still waiting for the API to load
+ return null;
+ }
+ return {children};
+ }
+);
+
+DeferredApiBootstrap.displayName = 'DeferredApiBootstrap';
+
+export default DeferredApiBootstrap;
diff --git a/packages/jsapi-bootstrap/src/index.ts b/packages/jsapi-bootstrap/src/index.ts
index e67002b81a..b529985fa7 100644
--- a/packages/jsapi-bootstrap/src/index.ts
+++ b/packages/jsapi-bootstrap/src/index.ts
@@ -1,4 +1,6 @@
export * from './ApiBootstrap';
export * from './ClientBootstrap';
+export * from './DeferredApiBootstrap';
export * from './useApi';
export * from './useClient';
+export * from './useDeferredApi';