Skip to content

Commit 51ebe1b

Browse files
authored
feat: Add support for useDeferredApi (#1725)
- Port over the functionality of GetPanelApiContext into a hook that can be easily used instead - Will be necessary to ensure widgets panels are wrapped in the correct API, as they are opened using portal panels and may not be wrapped in the same API - See DH-16249 for issue with wrong API being used in Enterprise
1 parent a3bea73 commit 51ebe1b

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import dh from '@deephaven/jsapi-shim';
3+
import { useContext } from 'react';
4+
import { TestUtils } from '@deephaven/utils';
5+
import { useApi } from './useApi';
6+
7+
const { asMock } = TestUtils;
8+
9+
jest.mock('react', () => ({
10+
...jest.requireActual('react'),
11+
useContext: jest.fn(),
12+
}));
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
expect.hasAssertions();
17+
18+
asMock(useContext).mockName('useContext');
19+
});
20+
21+
describe('useApi', () => {
22+
it('should return API context value', () => {
23+
asMock(useContext).mockReturnValue(dh);
24+
25+
const { result } = renderHook(() => useApi());
26+
expect(result.current).toBe(dh);
27+
});
28+
29+
it('should throw if context is null', () => {
30+
asMock(useContext).mockReturnValue(null);
31+
32+
const { result } = renderHook(() => useApi());
33+
expect(result.error).toEqual(
34+
new Error(
35+
'No API available in useApi. Was code wrapped in ApiBootstrap or ApiContext.Provider?'
36+
)
37+
);
38+
});
39+
});

packages/jsapi-bootstrap/src/useApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { dh as DhType } from '@deephaven/jsapi-types';
22
import { useContextOrThrow } from '@deephaven/react-hooks';
33
import { ApiContext } from './ApiBootstrap';
44

5+
/**
6+
* Retrieve the API for the current context.
7+
* @returns The API instance from the nearest ApiContext.Provider, or throws if none is set
8+
*/
59
export function useApi(): DhType {
610
return useContextOrThrow(
711
ApiContext,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import type { dh as DhType } from '@deephaven/jsapi-types';
3+
import { useContext } from 'react';
4+
import { TestUtils } from '@deephaven/utils';
5+
import { DeferredApiOptions, useDeferredApi } from './useDeferredApi';
6+
7+
const { asMock, createMockProxy, flushPromises } = TestUtils;
8+
9+
const dh1 = createMockProxy<DhType>();
10+
const dh2 = createMockProxy<DhType>();
11+
12+
jest.mock('react', () => ({
13+
...jest.requireActual('react'),
14+
useContext: jest.fn(),
15+
}));
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
asMock(useContext).mockName('useContext');
20+
});
21+
22+
describe('useApi', () => {
23+
it('should return API directly if a value is provided from useContext, whatever the options are', () => {
24+
asMock(useContext).mockReturnValue(dh1);
25+
26+
const { result } = renderHook(() => useDeferredApi());
27+
expect(result.current).toEqual([dh1, null]);
28+
29+
const { result: result2 } = renderHook(() =>
30+
useDeferredApi({ foo: 'bar' })
31+
);
32+
expect(result2.current).toEqual([dh1, null]);
33+
});
34+
35+
it('should resolve to the API value when it is provided from the function', async () => {
36+
asMock(useContext).mockReturnValue(async (options?: DeferredApiOptions) => {
37+
switch (options?.id) {
38+
case '1':
39+
return dh1;
40+
case '2':
41+
return dh2;
42+
default:
43+
throw new Error('Invalid id');
44+
}
45+
});
46+
47+
const { rerender, result } = renderHook(
48+
(options?: DeferredApiOptions) => useDeferredApi(options),
49+
{ initialProps: { id: '1' } }
50+
);
51+
await act(flushPromises);
52+
expect(result.current).toEqual([dh1, null]);
53+
54+
rerender({ id: '2' });
55+
await act(flushPromises);
56+
expect(result.current).toEqual([dh2, null]);
57+
58+
rerender({ id: '3' });
59+
await act(flushPromises);
60+
expect(result.current).toEqual([null, expect.any(Error)]);
61+
});
62+
63+
it('returns an error if the context is null', async () => {
64+
asMock(useContext).mockReturnValue(null);
65+
66+
const { result } = renderHook(() => useDeferredApi());
67+
expect(result.current).toEqual([null, expect.any(Error)]);
68+
});
69+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createContext, useContext, useEffect, useState } from 'react';
2+
import type { dh as DhType } from '@deephaven/jsapi-types';
3+
import { ApiContext } from './ApiBootstrap';
4+
5+
/** Options for retrieving the deferred */
6+
export type DeferredApiOptions = Record<string, unknown>;
7+
8+
export type DeferredApiFetcher = (
9+
options?: DeferredApiOptions
10+
) => Promise<DhType>;
11+
12+
export const DeferredApiContext = createContext<
13+
DhType | DeferredApiFetcher | null
14+
>(null);
15+
16+
/**
17+
* Retrieve the API for the current context, given the metadata provided.
18+
* The API may need to be loaded, and will return `null` until it is ready.
19+
* @returns A tuple with the API instance, and an error if one occurred.
20+
*/
21+
export function useDeferredApi(
22+
options?: Record<string, unknown>
23+
): [DhType | null, unknown | null] {
24+
const [api, setApi] = useState<DhType | null>(null);
25+
const [error, setError] = useState<unknown | null>(null);
26+
const deferredApi = useContext(DeferredApiContext);
27+
const contextApi = useContext(ApiContext);
28+
29+
useEffect(() => {
30+
if (deferredApi == null) {
31+
if (contextApi != null) {
32+
setApi(contextApi);
33+
setError(null);
34+
return;
35+
}
36+
setApi(null);
37+
setError(
38+
new Error(
39+
'No API available in useDeferredApi. Was code wrapped in ApiBootstrap or DeferredApiContext.Provider?'
40+
)
41+
);
42+
return;
43+
}
44+
let isCancelled = false;
45+
46+
async function loadApi() {
47+
if (typeof deferredApi === 'function') {
48+
try {
49+
const newApi = await deferredApi(options);
50+
if (!isCancelled) {
51+
setApi(newApi);
52+
setError(null);
53+
}
54+
} catch (e) {
55+
if (!isCancelled) {
56+
setApi(null);
57+
setError(e);
58+
}
59+
}
60+
} else {
61+
setApi(deferredApi);
62+
}
63+
}
64+
65+
loadApi();
66+
67+
return () => {
68+
isCancelled = true;
69+
};
70+
}, [contextApi, deferredApi, options]);
71+
72+
return [api, error];
73+
}
74+
75+
export default useDeferredApi;

0 commit comments

Comments
 (0)