Skip to content

Commit

Permalink
refactor(uselazy): rewrite internals by just handle an array of dynam…
Browse files Browse the repository at this point in the history
…ic imports

instead of handling a function's return value, is more simple just handle and array of functions and
resolve its promises using Promise.all

BREAKING CHANGE: now useLazy takes and array of functions instead of a single one.
  • Loading branch information
aneurysmjs committed Jan 22, 2020
1 parent 9399c5c commit 3059e0f
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 64 deletions.
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ or

## API


```typescript
// This it whats takes useLazy:
useLazy(
// function that returns a promise from a dynamic import
getModule: () => Promise<{ default: () => P }> | Array<Promise<{ default: () => P }>>,
useLazy<T>(
// array of functions that returns a promise from a dynamic import
// NOTE: please you should wrap this value inside of `useMemo`
importFns: Array<() => Promise<{ default: T }>>,
// this is were you decided when to execute the import
shouldImport: boolean
);
Expand All @@ -46,13 +48,17 @@ const Text = () => <p> Here's your beer </p>;
export default Text;
// App.tsx
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import useLazy from 'uselazy';
const imports = () => import('./Text');
const App = () => {
const [shouldImport, setShouldImport] = useState(false);
const { isLoading, result: SomeComponent } = useLazy(
() => import('./Text'),
// Preserves identity of "imports" so it can be safely add as a dependency of useEffect
// inside useLazy
useMemo(() => imports, []),
shouldImport
);
Expand Down Expand Up @@ -92,10 +98,14 @@ export default AnotherText;
import React, { useState } from 'react';
import useLazy from 'uselazy';
const imports = [ () => import('./Text'), () => import('./AnotherText')];
const App = () => {
const [shouldImport, setShouldImport] = useState(false);
const { isLoading, result: Components } = useLazy(
() => [import('./Text'), import('./AnotherText')],
// Preserves identity of "imports" so it can be safely add as a dependency of useEffect
// inside useLazy
useMemo(() => imports, []),
shouldImport
);
Expand Down Expand Up @@ -131,10 +141,14 @@ export default someUtils;
import React, { useState } from 'react';
import useLazy from 'uselazy';

const utilsImport = [() => import('./someUtils')];

const App = () => {
const [shouldImport, setShouldImport] = useState(false);
const { isLoading, result: utils } = useLazy(
() => import('./someUtils'),
// Preserves identity of "utilsImport" so it can be safely add as a dependency of useEffect
// inside useLazy
useMemo(() => utilsImport, []),
shouldImport
);

Expand All @@ -157,6 +171,16 @@ const App = () => {
};
```

## NOTE

The reason why I'm encouraging to wrap the imports array with `useMemo` is because of useEffect's array of dependencies,
that triggers a re-render whatever they change, so if you DON'T wrap it, you'll get an infinite rerendering loop because,
each render the imports array is different. [] === [] // false.

so I giving the developer total control of this, he decides whether the array can change.

more details here: [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/)

## LICENSE

[MIT](LICENSE)
50 changes: 33 additions & 17 deletions src/__test__/useLazy.test.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
/* eslint-disable @typescript-eslint/ban-ts-ignore */
import { renderHook } from '@testing-library/react-hooks';

import { ReactElement } from 'react';
import { ReactElement, useMemo } from 'react';

import useLazy from '../useLazy';

// type ExampleModule = () => Promise<typeof import('./Example')>;
type ExampleModule = () => Promise<{ default: () => ReactElement }>;
type ModuleType = Promise<{ default: () => ReactElement }>;
type ExampleModule = () => ModuleType;

const getModule: ExampleModule = () => import('./Example');

type GetModules = () => Array<Promise<{ default: () => ReactElement }>>;

const getModules: GetModules = () => [import('./Example'), import('./AnotherExample')];
const getModules = [
(): ModuleType => import('./Example'),
(): ModuleType => import('./AnotherExample'),
];

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

expect(renderHookResult.current.isLoading).toEqual(true);
Expand All @@ -34,7 +39,11 @@ describe('LazyComponent', () => {
it('should now how to handle and array of promises', async () => {
const shouldImport = true;
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy(getModules, shouldImport),
useLazy(
// @ts-ignore
useMemo(() => getModules, []),
shouldImport,
),
);

expect(renderHookResult.current.isLoading).toEqual(true);
Expand All @@ -56,7 +65,10 @@ describe('LazyComponent', () => {

const shouldImport = true;
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy(getUtil, shouldImport),
useLazy(
useMemo(() => [getUtil], []),
shouldImport,
),
);

expect(renderHookResult.current.isLoading).toEqual(true);
Expand All @@ -78,7 +90,10 @@ describe('LazyComponent', () => {

const shouldImport = true;
const { result: renderHookResult, waitForNextUpdate } = renderHook(() =>
useLazy(getUtil, shouldImport),
useLazy(
useMemo(() => [getUtil], []),
shouldImport,
),
);

expect(renderHookResult.current.isLoading).toEqual(true);
Expand All @@ -105,7 +120,10 @@ describe('LazyComponent', () => {

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

expect(renderHookResult.current.isLoading).toEqual(true);
Expand All @@ -124,15 +142,13 @@ describe('LazyComponent', () => {
});

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

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

const shouldImport = true;
Expand Down
24 changes: 13 additions & 11 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
export const FETCH_INIT = 'FETCH_INIT';
export const FETCH_SUCCESS = 'FETCH_SUCCESS';
export const FETCH_FAILURE = 'FETCH_FAILURE';
export const IMPORT_INIT = 'IMPORT_INIT';
export const IMPORT_SUCCESS = 'IMPORT_SUCCESS';
export const IMPORT_FAILURE = 'IMPORT_FAILURE';

interface DefaultImport<T> {
export interface DefaultImport<T> {
default: T;
}

export interface GetModule<T> {
(): Promise<DefaultImport<T>> | Array<Promise<DefaultImport<T>>>;
export interface ImportFn<T> {
(): Promise<DefaultImport<T>>;
}

export type Result<T> = T | Array<T> | null | Error;
// union type with `null` cuz' is the defaults
// and `undefined` cuz' it's something that might be returned from `Array.prototype.pop`
// when there's a single element on the modules array, so Typescript gets mad about it.
export type Result<T> = T | Array<T | undefined> | null | Error | undefined;

export interface State<T> {
isLoading: boolean;
result: Result<T>;
}

export type Action<T> =
| { readonly type: typeof FETCH_INIT }
| { readonly type: typeof FETCH_SUCCESS; payload: Result<T> }
| { readonly type: typeof FETCH_FAILURE; payload: Result<T> };
| { readonly type: typeof IMPORT_INIT }
| { readonly type: typeof IMPORT_SUCCESS; payload: Result<T> }
| { readonly type: typeof IMPORT_FAILURE; payload: Result<T> };
53 changes: 24 additions & 29 deletions src/useLazy.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,42 @@
import { useEffect, useCallback, useReducer, Reducer } from 'react';
import { useEffect, useReducer, Reducer } from 'react';

import handleThrow from './utils/handleThrow';
import handleImport from './utils/handleImport';

import { GetModule, Action, State, FETCH_INIT, FETCH_SUCCESS, FETCH_FAILURE } from './types';
import { ImportFn, Action, State, IMPORT_INIT, IMPORT_SUCCESS, IMPORT_FAILURE } from './types';

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

function makeReducer<T>(): Reducer<State<T>, Action<T>> {
return (state: State<T>, action: Action<T>): State<T> => {
return (state: State<T> = initialState, action: Action<T>): State<T> => {
switch (action.type) {
case FETCH_INIT:
case IMPORT_INIT:
return {
...state,
isLoading: true,
};
case FETCH_SUCCESS:
case IMPORT_SUCCESS:
return {
...state,
isLoading: false,
result: action.payload,
};
case FETCH_FAILURE:
case IMPORT_FAILURE:
return {
...state,
isLoading: false,
result: action.payload,
};
default:
throw new Error();
return state;
}
};
}

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

function useLazy<T>(getModule: GetModule<T>, shouldImport = false): State<T> {
// Preserves identity of "getModule" so it can be safely add as a dependency of useEffect
const resolver = useCallback(getModule, []);

function useLazy<T>(importFns: Array<ImportFn<T>>, shouldImport = false): State<T> {
const reducer = makeReducer<T>();

const [state, dispatch] = useReducer(reducer, initialState);
Expand All @@ -49,23 +47,20 @@ function useLazy<T>(getModule: GetModule<T>, shouldImport = false): State<T> {
if (!shouldImport) {
return;
}
dispatch({ type: FETCH_INIT });

const module = await resolver();
dispatch({ type: IMPORT_INIT });
// call each dynamic import inside `importFns` and resolve each promise
const modules = await Promise.all(importFns.map(i => i()));

if (module instanceof Array) {
const modules = await Promise.all(module);
dispatch({ type: FETCH_SUCCESS, payload: modules.map(m => m.default) });
}

if ('default' in module) {
dispatch({ type: FETCH_SUCCESS, payload: module.default });
}
dispatch({
type: IMPORT_SUCCESS,
// when there's more than one element, set and array, otherwise, just pluck the single element
payload: modules.length > 1 ? modules.map(handleImport) : handleImport(modules.pop()),
});
} catch (error) {
dispatch({ type: FETCH_FAILURE, payload: error });
dispatch({ type: IMPORT_FAILURE, payload: error });
}
})();
}, [resolver, shouldImport]);
}, [importFns, shouldImport]);

return handleThrow(state);
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/handleImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DefaultImport } from '../types';

export default function handleImport<T>(obj?: DefaultImport<T>): T | undefined {
return obj && obj.default;
}

0 comments on commit 3059e0f

Please sign in to comment.