Skip to content

Commit

Permalink
feat: Add support for useDeferredApi
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
mofojed committed Jan 11, 2024
1 parent a3bea73 commit f287d0c
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
39 changes: 39 additions & 0 deletions packages/jsapi-bootstrap/src/useApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { renderHook } from '@testing-library/react-hooks';
import dh from '@deephaven/jsapi-shim';
import { useContext } from 'react';
import { TestUtils } from '@deephaven/utils';
import { useApi } from './useApi';

const { asMock } = TestUtils;

jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(),
}));

beforeEach(() => {
jest.clearAllMocks();
expect.hasAssertions();

asMock(useContext).mockName('useContext');
});

describe('useApi', () => {
it('should return API context value', () => {
asMock(useContext).mockReturnValue(dh);

const { result } = renderHook(() => useApi());
expect(result.current).toBe(dh);
});

it('should throw if context is null', () => {
asMock(useContext).mockReturnValue(null);

const { result } = renderHook(() => useApi());
expect(result.error).toEqual(
new Error(
'No API available in useApi. Was code wrapped in ApiBootstrap or ApiContext.Provider?'
)
);
});
});
4 changes: 4 additions & 0 deletions packages/jsapi-bootstrap/src/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { dh as DhType } from '@deephaven/jsapi-types';
import { useContextOrThrow } from '@deephaven/react-hooks';
import { ApiContext } from './ApiBootstrap';

/**
* Retrieve the API for the current context.
* @returns The API instance from the nearest ApiContext.Provider, or throws if none is set
*/
export function useApi(): DhType {
return useContextOrThrow(
ApiContext,
Expand Down
69 changes: 69 additions & 0 deletions packages/jsapi-bootstrap/src/useDeferredApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { act, renderHook } from '@testing-library/react-hooks';
import type { dh as DhType } from '@deephaven/jsapi-types';
import { useContext } from 'react';
import { TestUtils } from '@deephaven/utils';
import { DeferredApiOptions, useDeferredApi } from './useDeferredApi';

const { asMock, createMockProxy, flushPromises } = TestUtils;

const dh1 = createMockProxy<DhType>();
const dh2 = createMockProxy<DhType>();

jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(),
}));

beforeEach(() => {
jest.clearAllMocks();
asMock(useContext).mockName('useContext');
});

describe('useApi', () => {
it('should return API directly if a value is provided from useContext, whatever the options are', () => {
asMock(useContext).mockReturnValue(dh1);

const { result } = renderHook(() => useDeferredApi());
expect(result.current).toEqual([dh1, null]);

const { result: result2 } = renderHook(() =>
useDeferredApi({ foo: 'bar' })
);
expect(result2.current).toEqual([dh1, null]);
});

it('should resolve to the API value when it is provided from the function', async () => {
asMock(useContext).mockReturnValue(async (options?: DeferredApiOptions) => {
switch (options?.id) {
case '1':
return dh1;
case '2':
return dh2;
default:
throw new Error('Invalid id');
}
});

const { rerender, result } = renderHook(
(options?: DeferredApiOptions) => useDeferredApi(options),
{ initialProps: { id: '1' } }
);
await act(flushPromises);
expect(result.current).toEqual([dh1, null]);

rerender({ id: '2' });
await act(flushPromises);
expect(result.current).toEqual([dh2, null]);

rerender({ id: '3' });
await act(flushPromises);
expect(result.current).toEqual([null, expect.any(Error)]);
});

it('returns an error if the context is null', async () => {
asMock(useContext).mockReturnValue(null);

const { result } = renderHook(() => useDeferredApi());
expect(result.current).toEqual([null, expect.any(Error)]);
});
});
75 changes: 75 additions & 0 deletions packages/jsapi-bootstrap/src/useDeferredApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { dh as DhType } from '@deephaven/jsapi-types';
import { ApiContext } from './ApiBootstrap';

/** Options for retrieving the deferred */
export type DeferredApiOptions = Record<string, unknown>;

export type DeferredApiFetcher = (
options?: DeferredApiOptions
) => Promise<DhType>;

export const DeferredApiContext = createContext<
DhType | DeferredApiFetcher | null
>(null);

/**
* Retrieve the API for the current context, given the metadata provided.
* The API may need to be loaded, and will return `null` until it is ready.
* @returns A tuple with the API instance, and an error if one occurred.
*/
export function useDeferredApi(
options?: Record<string, unknown>
): [DhType | null, unknown | null] {
const [api, setApi] = useState<DhType | null>(null);
const [error, setError] = useState<unknown | null>(null);
const deferredApi = useContext(DeferredApiContext);
const contextApi = useContext(ApiContext);

useEffect(() => {
if (deferredApi == null) {
if (contextApi != null) {
setApi(contextApi);
setError(null);
return;
}
setApi(null);
setError(
new Error(
'No API available in useDeferredApi. Was code wrapped in ApiBootstrap or DeferredApiContext.Provider?'
)
);
return;
}
let isCancelled = false;

async function loadApi() {
if (typeof deferredApi === 'function') {
try {
const newApi = await deferredApi(options);
if (!isCancelled) {
setApi(newApi);
setError(null);
}
} catch (e) {
if (!isCancelled) {
setApi(null);
setError(e);
}
}
} else {
setApi(deferredApi);
}
}

loadApi();

return () => {
isCancelled = true;
};
}, [contextApi, deferredApi, options]);

return [api, error];
}

export default useDeferredApi;

0 comments on commit f287d0c

Please sign in to comment.