Skip to content

Commit

Permalink
feat(uselazy): return object with result and loading status
Browse files Browse the repository at this point in the history
instead of returning null while the import is resolving, I've decided to return a object instead
which has the loading status while import is resolving and result which will contain the import's
contents
  • Loading branch information
aneurysmjs committed Oct 30, 2019
1 parent 05ecdf4 commit 06f2f43
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 31 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import useLazy from 'uselazy';
const App = () => {
const [cond, setCond] = useState(false);
const SomeComponent = useLazy({
const { isLoading, result: SomeComponent } = useLazy({
getModule: () => import('./Text'),
shouldImport: cond,
onFynally: () => console.log('ахуититиьна')
Expand All @@ -68,6 +68,8 @@ const App = () => {
Buy me a beer
</button>

{isLoading && <span>some spinner</span>}

{SomeComponent && <SomeComponent />}
</div>
);
Expand Down Expand Up @@ -97,7 +99,7 @@ import useLazy from 'uselazy';
const App = () => {
const [cond, setCond] = useState(false);
const Comonents = useLazy({
const { isLoading, result: Components } = useLazy({
getModule: () => [import('./Text'), import('./AnotherText')],
shouldImport: cond,
onFynally: () => console.log('ахуититиьна')
Expand All @@ -110,6 +112,8 @@ const App = () => {
Buy me lots of beers
</button>

{isLoading && <span>some spinner</span>}

{Components && Components.map(Component => <Component />)}
</div>
);
Expand Down
57 changes: 37 additions & 20 deletions src/__test__/useLazy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,103 +15,120 @@ const getModules: GetModules = () => [import('./Example'), import('./AnotherExam

describe('LazyComponent', () => {
it('should render "null" at first and then resolve promise', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy({
getModule,
shouldImport: true,
}),
);
expect(result.current).toEqual(null);

expect(renderHookResult.current.isLoading).toEqual(true);
expect(renderHookResult.current.result).toEqual(null);

await waitForNextUpdate();

expect(result.current).not.toBe(undefined);
expect(typeof result.current).toBe('function');
expect(renderHookResult.current.isLoading).toEqual(false);
expect(renderHookResult.current.result).not.toBe(undefined);
expect(typeof renderHookResult.current.result).toBe('function');
});

it('should call "finally" handler when the promised is resolve', async () => {
const handleFinally = jest.fn();
const { result, waitForNextUpdate } = renderHook(() =>
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy({
getModule,
shouldImport: true,
onFynally: handleFinally,
}),
);
expect(result.current).toEqual(null);

expect(renderHookResult.current.isLoading).toEqual(true);
expect(renderHookResult.current.result).toEqual(null);

await waitForNextUpdate();

expect(result.current).not.toBe(undefined);
expect(typeof result.current).toBe('function');
expect(renderHookResult.current.isLoading).toEqual(false);
expect(renderHookResult.current.result).not.toBe(undefined);
expect(typeof renderHookResult.current.result).toBe('function');
expect(handleFinally).toHaveBeenCalledTimes(1);
});

it('should now how to handle and array of promises', async () => {
const handleFinally = jest.fn();
const { result, waitForNextUpdate } = renderHook(() =>
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy({
getModule: getModules,
shouldImport: true,
onFynally: handleFinally,
}),
);
expect(result.current).toEqual(null);

expect(renderHookResult.current.isLoading).toEqual(true);
expect(renderHookResult.current.result).toEqual(null);

await waitForNextUpdate();

expect(Array.isArray(result.current)).toBe(true);
expect(renderHookResult.current.isLoading).toEqual(false);
expect(Array.isArray(renderHookResult.current.result)).toBe(true);
// @ts-ignore
expect(result.current.every(f => typeof f === 'function')).toBe(true);
expect(renderHookResult.current.result.every(f => typeof f === 'function')).toBe(true);
expect(handleFinally).toHaveBeenCalledTimes(1);
});

describe('handle exceptions', () => {
it('should throw for a single import', async () => {
// @ts-ignore - just for testing purposes
// @ts-ignore - just to avoid Typescript's "can't find module"
type WrongModule = typeof import('./wrong/Example');
// @ts-ignore - just for testing purposes
// @ts-ignore - just to avoid Typescript's "can't find module"
const wrongModule = (): WrongModule => import('./wrong/Example'); // eslint-disable-line import/no-unresolved

const { result, waitForNextUpdate } = renderHook(() =>
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy({
getModule: wrongModule,
shouldImport: true,
}),
);

expect(renderHookResult.current.isLoading).toEqual(true);

await waitForNextUpdate();

expect(() => {
expect(result.current).not.toBe(undefined);
expect(renderHookResult.current.result).not.toBe(undefined);
}).toThrow(
Error(`useLazy Error: Cannot find module './wrong/Example' from 'useLazy.test.tsx'`),
);

expect(renderHookResult.error).toEqual(
Error(`useLazy Error: Cannot find module './wrong/Example' from 'useLazy.test.tsx'`),
);
});

it('should throw for multiple imports', async () => {
type WrongModules = () => Array<
// @ts-ignore
// @ts-ignore - just to avoid Typescript's "can't find module"
Promise<typeof import('./Example') | typeof import('./AnotherWrongModule')>
>;

const wrongModules: WrongModules = () => [
import('./Example'),
// @ts-ignore
// @ts-ignore - just to avoid Typescript's "can't find module"
import('./AnotherWrongModule'), // eslint-disable-line import/no-unresolved
];

const { result, waitForNextUpdate } = renderHook(() =>
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy({
getModule: wrongModules,
shouldImport: true,
}),
);

expect(renderHookResult.current.isLoading).toEqual(true);

await waitForNextUpdate();

expect(() => {
expect(result.current).not.toBe(undefined);
expect(renderHookResult.current.result).not.toBe(undefined);
}).toThrow(
Error(`useLazy Error: Cannot find module './AnotherWrongModule' from 'useLazy.test.tsx'`),
);
Expand Down
35 changes: 26 additions & 9 deletions src/useLazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,50 @@ interface LazyObj<P> {
onFynally?: () => void;
}

/**
* is much better to have all in one object, that allows the arguments to come
* in any order and if there's anyone we don't need, we can simply ignore them
* or anything that is missing we can simply provide defaults
*/
interface UseLazyResult<P> {
isLoading: boolean;
result: (() => P) | Array<() => P> | null;
}

const initialState = {
isLoading: false,
result: null,
};

function useLazy<P>({
getModule,
shouldImport = false,
onFynally = (): void => {},
}: LazyObj<P>): (() => P) | Array<() => P> | null {
const [AsyncModule, setAsyncModule] = useState<(() => P) | Array<() => P> | null>(null);
}: LazyObj<P>): UseLazyResult<P> {
const [AsyncModule, setAsyncModule] = useState<UseLazyResult<P>>(initialState);

useEffect(() => {
(async (): Promise<void> => {
try {
if (!shouldImport) {
return;
}

setAsyncModule({
isLoading: true,
result: null,
});

const module = await getModule();

if (module instanceof Array) {
const modules = await Promise.all(module);
setAsyncModule(modules.map(m => m.default));
setAsyncModule({
isLoading: false,
result: modules.map(m => m.default),
});
}

if ('default' in module) {
setAsyncModule(() => module.default);
setAsyncModule({
isLoading: false,
result: module.default,
});
}
} catch (err) {
setAsyncModule(err);
Expand Down

0 comments on commit 06f2f43

Please sign in to comment.