Skip to content

Commit 6e59006

Browse files
committed
feat: importedModule helper to load modules in declarative way
1 parent 45fc8e9 commit 6e59006

File tree

16 files changed

+218
-23
lines changed

16 files changed

+218
-23
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,36 @@ What you could load using `useImported`? Everything - `imported` itself is using
174174
175175
> 💡 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.
176176
177+
🤔 Keep in mind - __everything here is using `useImported`__, and __you can build whatever you need__ using just it.
178+
179+
### Module
180+
A slim helper to help handle `modules`, you might require using `useImported` in a component way
181+
```js
182+
import {importedModule, ImportedModule} from 'react-imported-component';
183+
184+
const Moment = importedModule(() => import('moment'));
185+
186+
<Moment fallback="long time ago">
187+
{(momentjs /* default imports are auto-imported*/) => momentjs(date).fromNow()}
188+
</Moment>
189+
```
190+
> Yes, this example was taken from [loadable.lib](https://www.smooth-code.com/open-source/loadable-components/docs/library-splitting/)
191+
192+
Can I also use a ref, populated when the library is loaded? No, you cant. Use `useImported` for any special case like this.
193+
194+
Plus, there is a Component helper:
195+
```js
196+
<ImportedModule
197+
// import is just a _trackable_ promise - do what ever you want
198+
import={() => import('moment').then(({momentDefault})=> momentDefault(date).fromNow()}
199+
fallback="long time ago"
200+
>
201+
{(fromNow) => fromNow()}
202+
</ImportedModule>
203+
```
204+
205+
> ImportedModule will throw a promise to the nearest Suspense boundary if no `fallback` provided.
206+
177207
## Babel macro
178208
If you could not use babel plugin, but do have `babel-plugin-macro` (like CRA) - consider using macro API:
179209
```js
@@ -224,6 +254,10 @@ If you have `imported` definition in one file, and use it from another - just `i
224254
- `loading` - is it loading right now?
225255
- `loadable` - the underlying `Loadable` object
226256
- `retry` - retry action (in case of error)
257+
258+
Hints:
259+
- use `options.import=false` to perform conditional import - `importFunction` would not be used if this option set to `false.
260+
- use `options.track=true` to perform SSR only import - to usage would be tracked if this option set to `false.
227261

228262
##### Misc
229263
There is also API method, unique for imported-component, which could be useful on the client side

__tests__/module.spec.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
import {mount} from 'enzyme';
3+
import {act} from "react-dom/test-utils";
4+
5+
import {importedModule, ImportedModule} from '../src/Module';
6+
import {done} from "../src/loadable";
7+
8+
describe('Module Component', () => {
9+
10+
describe('hoc', () => {
11+
it('should load component', async () => {
12+
const Component = importedModule(() => Promise.resolve((x: number) => x + 42));
13+
14+
const wrapper = mount(<Component fallback={"loading"}>{(fn) => <span>{fn(42)}</span>}</Component>);
15+
16+
expect(wrapper.update().html()).toContain("loading");
17+
18+
await act(async () => {
19+
await done();
20+
});
21+
22+
expect(wrapper.update().html()).toContain("84");
23+
});
24+
});
25+
26+
describe('component', () => {
27+
it('should load component', async () => {
28+
const importer = () => Promise.resolve((x: number) => x + 42);
29+
30+
const wrapper = mount(
31+
<ImportedModule
32+
import={importer}
33+
fallback={"loading"}
34+
>
35+
{(fn) => <span>{fn(42)}</span>}
36+
</ImportedModule>
37+
);
38+
39+
expect(wrapper.update().html()).toContain("loading");
40+
41+
await act(async () => {
42+
await done();
43+
});
44+
45+
expect(wrapper.update().html()).toContain("84");
46+
});
47+
})
48+
});

__tests__/useImported.spec.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import {mount} from 'enzyme';
33
import {act} from "react-dom/test-utils";
44
import {useLoadable, useImported} from '../src/useImported';
5-
import {toLoadable, getLoadable} from "../src/loadable";
5+
import {toLoadable, getLoadable, done} from "../src/loadable";
66
import {drainHydrateMarks} from "../src";
77

88

@@ -15,8 +15,6 @@ describe('useLoadable', () => {
1515
const Component = () => {
1616
const used = useLoadable(loadable);
1717
}
18-
19-
2018
});
2119
});
2220

@@ -91,4 +89,39 @@ describe('useImported', () => {
9189
expect(wrapper.html()).toContain("error");
9290
expect(drainHydrateMarks()).toEqual([]);
9391
});
92+
93+
it('conditional import', async () => {
94+
const importer = () => importedWrapper('imported_mark1_component', Promise.resolve(() => <span>loaded!</span>));
95+
96+
const Comp = ({loadit}) => {
97+
const {loading, imported: Component,} = useImported(importer, undefined, {import: loadit});
98+
99+
if (Component) {
100+
return <Component/>
101+
}
102+
103+
if (loading) {
104+
return <span>loading</span>;
105+
}
106+
return <span>nothing</span>;
107+
};
108+
109+
const wrapper = mount(<Comp loadit={false}/>);
110+
expect(wrapper.html()).toContain("nothing");
111+
112+
await act(async () => {
113+
await Promise.resolve();
114+
});
115+
116+
expect(wrapper.update().html()).toContain("nothing");
117+
wrapper.setProps({loadit: true});
118+
expect(wrapper.update().html()).toContain("nothing");
119+
120+
await act(async () => {
121+
await done();
122+
});
123+
124+
expect(wrapper.update().html()).toContain("loaded!");
125+
expect(drainHydrateMarks()).toEqual(["mark1"]);
126+
})
94127
});

examples/CRA/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"typescript": "3.3.3"
1616
},
1717
"scripts": {
18-
"start": "react-scripts start",
18+
"start": "imported-components src async-imports.js && react-scripts start",
1919
"build": "react-scripts build",
2020
"test": "react-scripts test --env=jsdom",
2121
"eject": "react-scripts eject"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"crc-32": "^1.2.0",
6464
"detect-node": "^2.0.4",
6565
"prop-types": "^15.6.2",
66-
"react-uid": "^2.2.0",
6766
"scan-directory": "^1.0.0",
6867
"tslib": "^1.10.0"
6968
},

src/Component.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as React from 'react';
22
import {ReactElement} from "react";
3-
import {uid} from "react-uid";
43

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

2120
if (Component) {
2221
// importedUUID for cache busting
23-
return <Component {...props.forwardProps} ref={props.forwardRef} importedUUID={uid(Component)}/>
22+
return <Component {...props.forwardProps} ref={props.forwardRef} />
2423
}
2524

2625
if (loading) {

src/HOC.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
2+
import {useMemo} from "react";
3+
24
import {ImportedComponent} from './Component';
35
import {getLoadable} from './loadable';
46
import {ComponentOptions, DefaultComponentImport, HOCOptions, HOCType, LazyImport} from "./types";
57
import {useLoadable} from "./useImported";
6-
import {useMemo} from "react";
78
import {asDefault} from "./utils";
89
import {isBackend} from "./detectBackend";
910

src/Module.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from "react";
2+
import {
3+
DefaultImport,
4+
FullImportModuleProps,
5+
HOCModuleType,
6+
ImportModuleProps,
7+
8+
} from "./types";
9+
import {useImported} from "./useImported";
10+
import {getLoadable} from "./loadable";
11+
12+
13+
export function ImportedModule<T>(props: FullImportModuleProps<T>) {
14+
const {error, loadable, imported: module} = useImported(props.import);
15+
if (error) {
16+
throw error;
17+
}
18+
if (module) {
19+
return props.children(module);
20+
}
21+
if (!props.fallback){
22+
throw loadable.resolution;
23+
}
24+
return props.fallback as any;
25+
}
26+
27+
export function importedModule<T>(
28+
loaderFunction: DefaultImport<T>
29+
): HOCModuleType<T> {
30+
const loadable = getLoadable(loaderFunction);
31+
32+
const Module = ((props: ImportModuleProps<T>) => (
33+
<ImportedModule
34+
{...props}
35+
import={loadable}
36+
fallback={props.fallback}
37+
/>
38+
)) as HOCModuleType<T>;
39+
40+
Module.preload = () => {
41+
loadable.load().catch(() => ({}));
42+
43+
return loadable.resolution;
44+
};
45+
Module.done = loadable.resolution;
46+
47+
return Module;
48+
}

src/babel.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const templateOptions = {
3030
placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/,
3131
};
3232

33-
export const createTransformer = ({types: t, template}: any) => {
33+
export const createTransformer = ({types: t, template}: any, excludeMacro = false) => {
3434
const headerTemplate = template(`var importedWrapper = function(marker, realImport) {
3535
if (typeof __deoptimization_sideEffect__ !== 'undefined') {
3636
__deoptimization_sideEffect__(marker, realImport);
@@ -50,6 +50,10 @@ export const createTransformer = ({types: t, template}: any) => {
5050
traverse(programPath: any, fileName: string) {
5151
programPath.traverse({
5252
ImportDeclaration(path: any) {
53+
if(excludeMacro){
54+
return;
55+
}
56+
5357
const source = path.node.source.value;
5458
if (source === 'react-imported-component/macro') {
5559
const {specifiers} = path.node;

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export const settings = {
66
hot: (!!module as any).hot,
77
SSR: isBackend,
88
rethrowErrors: process.env.NODE_ENV !== 'production',
9-
fileFilter: rejectNetwork
9+
fileFilter: rejectNetwork,
10+
updateOnReload: false,
1011
};
1112

1213
export const setConfiguration = (config: Partial<typeof settings>) => {

0 commit comments

Comments
 (0)