-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
4 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?' | ||
) | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |