Skip to content

Commit

Permalink
feat: importedModule helper to load modules in declarative way
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Sep 25, 2019
1 parent 45fc8e9 commit 6e59006
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 23 deletions.
34 changes: 34 additions & 0 deletions README.md
Expand Up @@ -174,6 +174,36 @@ What you could load using `useImported`? Everything - `imported` itself is using
> 馃挕 did you know that there is another hook based solution to load _"something might might need"_? The [use-sidecar](https://github.com/theKashey/use-sidecar) pattern.
馃 Keep in mind - __everything here is using `useImported`__, and __you can build whatever you need__ using just it.

### Module
A slim helper to help handle `modules`, you might require using `useImported` in a component way
```js
import {importedModule, ImportedModule} from 'react-imported-component';

const Moment = importedModule(() => import('moment'));

<Moment fallback="long time ago">
{(momentjs /* default imports are auto-imported*/) => momentjs(date).fromNow()}
</Moment>
```
> Yes, this example was taken from [loadable.lib](https://www.smooth-code.com/open-source/loadable-components/docs/library-splitting/)
Can I also use a ref, populated when the library is loaded? No, you cant. Use `useImported` for any special case like this.

Plus, there is a Component helper:
```js
<ImportedModule
// import is just a _trackable_ promise - do what ever you want
import={() => import('moment').then(({momentDefault})=> momentDefault(date).fromNow()}
fallback="long time ago"
>
{(fromNow) => fromNow()}
</ImportedModule>
```

> ImportedModule will throw a promise to the nearest Suspense boundary if no `fallback` provided.
## Babel macro
If you could not use babel plugin, but do have `babel-plugin-macro` (like CRA) - consider using macro API:
```js
Expand Down Expand Up @@ -224,6 +254,10 @@ If you have `imported` definition in one file, and use it from another - just `i
- `loading` - is it loading right now?
- `loadable` - the underlying `Loadable` object
- `retry` - retry action (in case of error)

Hints:
- use `options.import=false` to perform conditional import - `importFunction` would not be used if this option set to `false.
- use `options.track=true` to perform SSR only import - to usage would be tracked if this option set to `false.

##### Misc
There is also API method, unique for imported-component, which could be useful on the client side
Expand Down
48 changes: 48 additions & 0 deletions __tests__/module.spec.tsx
@@ -0,0 +1,48 @@
import * as React from 'react';
import {mount} from 'enzyme';
import {act} from "react-dom/test-utils";

import {importedModule, ImportedModule} from '../src/Module';
import {done} from "../src/loadable";

describe('Module Component', () => {

describe('hoc', () => {
it('should load component', async () => {
const Component = importedModule(() => Promise.resolve((x: number) => x + 42));

const wrapper = mount(<Component fallback={"loading"}>{(fn) => <span>{fn(42)}</span>}</Component>);

expect(wrapper.update().html()).toContain("loading");

await act(async () => {
await done();
});

expect(wrapper.update().html()).toContain("84");
});
});

describe('component', () => {
it('should load component', async () => {
const importer = () => Promise.resolve((x: number) => x + 42);

const wrapper = mount(
<ImportedModule
import={importer}
fallback={"loading"}
>
{(fn) => <span>{fn(42)}</span>}
</ImportedModule>
);

expect(wrapper.update().html()).toContain("loading");

await act(async () => {
await done();
});

expect(wrapper.update().html()).toContain("84");
});
})
});
39 changes: 36 additions & 3 deletions __tests__/useImported.spec.tsx
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import {mount} from 'enzyme';
import {act} from "react-dom/test-utils";
import {useLoadable, useImported} from '../src/useImported';
import {toLoadable, getLoadable} from "../src/loadable";
import {toLoadable, getLoadable, done} from "../src/loadable";
import {drainHydrateMarks} from "../src";


Expand All @@ -15,8 +15,6 @@ describe('useLoadable', () => {
const Component = () => {
const used = useLoadable(loadable);
}


});
});

Expand Down Expand Up @@ -91,4 +89,39 @@ describe('useImported', () => {
expect(wrapper.html()).toContain("error");
expect(drainHydrateMarks()).toEqual([]);
});

it('conditional import', async () => {
const importer = () => importedWrapper('imported_mark1_component', Promise.resolve(() => <span>loaded!</span>));

const Comp = ({loadit}) => {
const {loading, imported: Component,} = useImported(importer, undefined, {import: loadit});

if (Component) {
return <Component/>
}

if (loading) {
return <span>loading</span>;
}
return <span>nothing</span>;
};

const wrapper = mount(<Comp loadit={false}/>);
expect(wrapper.html()).toContain("nothing");

await act(async () => {
await Promise.resolve();
});

expect(wrapper.update().html()).toContain("nothing");
wrapper.setProps({loadit: true});
expect(wrapper.update().html()).toContain("nothing");

await act(async () => {
await done();
});

expect(wrapper.update().html()).toContain("loaded!");
expect(drainHydrateMarks()).toEqual(["mark1"]);
})
});
2 changes: 1 addition & 1 deletion examples/CRA/package.json
Expand Up @@ -15,7 +15,7 @@
"typescript": "3.3.3"
},
"scripts": {
"start": "react-scripts start",
"start": "imported-components src async-imports.js && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
Expand Down
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -63,7 +63,6 @@
"crc-32": "^1.2.0",
"detect-node": "^2.0.4",
"prop-types": "^15.6.2",
"react-uid": "^2.2.0",
"scan-directory": "^1.0.0",
"tslib": "^1.10.0"
},
Expand Down
3 changes: 1 addition & 2 deletions src/Component.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import {ReactElement} from "react";
import {uid} from "react-uid";

import {settings} from './config';
import {ComponentOptions} from "./types";
Expand All @@ -20,7 +19,7 @@ function ImportedComponent<P, K>(props: ComponentOptions<P, K>): ReactElement |

if (Component) {
// importedUUID for cache busting
return <Component {...props.forwardProps} ref={props.forwardRef} importedUUID={uid(Component)}/>
return <Component {...props.forwardProps} ref={props.forwardRef} />
}

if (loading) {
Expand Down
3 changes: 2 additions & 1 deletion src/HOC.tsx
@@ -1,9 +1,10 @@
import * as React from 'react';
import {useMemo} from "react";

import {ImportedComponent} from './Component';
import {getLoadable} from './loadable';
import {ComponentOptions, DefaultComponentImport, HOCOptions, HOCType, LazyImport} from "./types";
import {useLoadable} from "./useImported";
import {useMemo} from "react";
import {asDefault} from "./utils";
import {isBackend} from "./detectBackend";

Expand Down
48 changes: 48 additions & 0 deletions src/Module.tsx
@@ -0,0 +1,48 @@
import * as React from "react";
import {
DefaultImport,
FullImportModuleProps,
HOCModuleType,
ImportModuleProps,

} from "./types";
import {useImported} from "./useImported";
import {getLoadable} from "./loadable";


export function ImportedModule<T>(props: FullImportModuleProps<T>) {
const {error, loadable, imported: module} = useImported(props.import);
if (error) {
throw error;
}
if (module) {
return props.children(module);
}
if (!props.fallback){
throw loadable.resolution;
}
return props.fallback as any;
}

export function importedModule<T>(
loaderFunction: DefaultImport<T>
): HOCModuleType<T> {
const loadable = getLoadable(loaderFunction);

const Module = ((props: ImportModuleProps<T>) => (
<ImportedModule
{...props}
import={loadable}
fallback={props.fallback}
/>
)) as HOCModuleType<T>;

Module.preload = () => {
loadable.load().catch(() => ({}));

return loadable.resolution;
};
Module.done = loadable.resolution;

return Module;
}
6 changes: 5 additions & 1 deletion src/babel.ts
Expand Up @@ -30,7 +30,7 @@ const templateOptions = {
placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/,
};

export const createTransformer = ({types: t, template}: any) => {
export const createTransformer = ({types: t, template}: any, excludeMacro = false) => {
const headerTemplate = template(`var importedWrapper = function(marker, realImport) {
if (typeof __deoptimization_sideEffect__ !== 'undefined') {
__deoptimization_sideEffect__(marker, realImport);
Expand All @@ -50,6 +50,10 @@ export const createTransformer = ({types: t, template}: any) => {
traverse(programPath: any, fileName: string) {
programPath.traverse({
ImportDeclaration(path: any) {
if(excludeMacro){
return;
}

const source = path.node.source.value;
if (source === 'react-imported-component/macro') {
const {specifiers} = path.node;
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Expand Up @@ -6,7 +6,8 @@ export const settings = {
hot: (!!module as any).hot,
SSR: isBackend,
rethrowErrors: process.env.NODE_ENV !== 'production',
fileFilter: rejectNetwork
fileFilter: rejectNetwork,
updateOnReload: false,
};

export const setConfiguration = (config: Partial<typeof settings>) => {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Expand Up @@ -7,6 +7,7 @@ import {
assignImportedComponents
} from './loadable';
import {ImportedComponent as ComponentLoader} from './Component';
import {ImportedModule, importedModule} from './Module';
import {ImportedStream} from "./context";
import LazyBoundary from './LazyBoundary'
import {setConfiguration} from './config';
Expand All @@ -29,12 +30,14 @@ export {
loadByChunkname,

ComponentLoader,
ImportedModule,
loadableResource,

ImportedStream,
setConfiguration,

imported,
importedModule,
lazy,
LazyBoundary,
remapImports,
Expand Down
6 changes: 5 additions & 1 deletion src/loadable.ts
Expand Up @@ -146,7 +146,11 @@ export function toLoadable<T>(firstImportFunction: Promised<T>, autoImport = tru
LOADABLE_SIGNATURE.set(functionSignature, loadable);
assingLoadableMark(mark, loadable);
} else {
console.warn('react-imported-component: no mark found at', importFunction);
console.warn(
'react-imported-component: no mark found at',
importFunction,
'Please check babel plugin or macro setup, as well as imported-component\'s limitations. See https://github.com/theKashey/react-imported-component/issues/147'
);
}

// trigger preload on the server side
Expand Down
7 changes: 3 additions & 4 deletions src/macro.ts
Expand Up @@ -9,6 +9,7 @@ function getMacroType(tagName: string) {
case "lazy":
case "useImported":
return "react-imported-component";

case "assignImportedComponents":
return "react-imported-component/boot";

Expand All @@ -22,7 +23,7 @@ function macro({references, state, babel}: any) {
const fileName = state.file.opts.filename;

const imports: Record<string, string[]> = {};
const transformer = createTransformer(babel);
const transformer = createTransformer(babel, true);

Object
.keys(references)
Expand All @@ -42,10 +43,8 @@ function macro({references, state, babel}: any) {
}
});

transformer.traverse(state.file.path, fileName);

addReactImports(babel, state, imports);

transformer.traverse(state.file.path, fileName);
transformer.finish(state.file.path.node, fileName);
}

Expand Down
23 changes: 22 additions & 1 deletion src/types.ts
@@ -1,4 +1,4 @@
import {ComponentType, ForwardRefExoticComponent, Ref, ReactElement} from "react";
import {ComponentType, ForwardRefExoticComponent, Ref, ReactElement, ReactNode} from "react";

export interface DefaultImportedComponent<P> {
default: ComponentType<P>;
Expand Down Expand Up @@ -61,6 +61,7 @@ export interface Loadable<T> {
then(callback: (x: T) => void, err: () => void): Promise<any>;
}


export type ComponentOptions<P, K> = {
loadable: DefaultComponentImport<P> | Loadable<ComponentType<P>>,

Expand Down Expand Up @@ -90,6 +91,26 @@ export type HOCType<P, K> =
ForwardRefExoticComponent<K & { importedProps?: Partial<ComponentOptions<P, K>> }> &
AdditionalHOC;

export interface ImportModuleHOCProps {
fallback: NonNullable<ReactNode>|null;
}

export interface ImportModuleProps<T> extends ImportModuleHOCProps {
children: (arg: T) => ReactElement | null;
}


export interface FullImportModuleProps<T> extends ImportModuleProps<T> {
import: DefaultImport<T> | Loadable<T>;
}

export type ModuleFC<T> = (props: ImportModuleProps<T>) => ReactElement | null;

export type HOCModuleType<T> =
ModuleFC<T> &
AdditionalHOC;


export interface HOC {
<P, K = P>(loader: DefaultComponentImport<P>, options?: Partial<ComponentOptions<P, K>> & HOCOptions): HOCType<P, K>;
}
Expand Down

0 comments on commit 6e59006

Please sign in to comment.