diff --git a/package.json b/package.json index d2eacdffe..3ddced5dd 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "bundlesize": [ { "path": "./packages/component/dist/loadable.min.js", - "maxSize": "2.5 kB" + "maxSize": "3.5 kB" }, { "path": "./packages/component/dist/loadable.esm.js", - "maxSize": "3.5 kB" + "maxSize": "4.5 kB" } ], "scripts": { diff --git a/packages/component/package.json b/packages/component/package.json index 6b0f3db92..9b16bcfa4 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@babel/runtime": "^7.7.7", - "hoist-non-react-statics": "^3.3.1" + "hoist-non-react-statics": "^3.3.1", + "react-is": "^16.12.0" } } diff --git a/packages/component/src/createLoadable.js b/packages/component/src/createLoadable.js index 261a09acf..1d00ee10a 100644 --- a/packages/component/src/createLoadable.js +++ b/packages/component/src/createLoadable.js @@ -1,5 +1,7 @@ /* eslint-disable no-use-before-define, react/no-multi-comp, no-underscore-dangle */ import React from 'react' +import * as ReactIs from 'react-is' +import hoistNonReactStatics from 'hoist-non-react-statics' import { invariant } from './util' import Context from './Context' @@ -19,7 +21,7 @@ const withChunkExtractor = Component => props => ( const identity = v => v -function createLoadable({ resolve = identity, render, onLoad }) { +function createLoadable({ defaultResolveComponent = identity, render, onLoad }) { function loadable(loadableConstructor, options = {}) { const ctor = resolveConstructor(loadableConstructor) const cache = {} @@ -33,6 +35,19 @@ function createLoadable({ resolve = identity, render, onLoad }) { return null } + function resolve(module, props, Loadable) { + const Component = options.resolveComponent + ? options.resolveComponent(module, props) + : defaultResolveComponent(module) + if (options.resolveComponent && !ReactIs.isValidElementType(Component)) { + throw new Error(`resolveComponent returned something that is not a React component!`) + } + hoistNonReactStatics(Loadable, Component, { + preload: true, + }) + return Component; + } + class InnerLoadable extends React.Component { static getDerivedStateFromProps(props, state) { const cacheKey = getCacheKey(props) @@ -128,7 +143,7 @@ function createLoadable({ resolve = identity, render, onLoad }) { try { const loadedModule = ctor.requireSync(this.props) - const result = resolve(loadedModule, { Loadable }) + const result = resolve(loadedModule, this.props, Loadable) this.state.result = result this.state.loading = false } catch (error) { @@ -154,13 +169,13 @@ function createLoadable({ resolve = identity, render, onLoad }) { this.promise = ctor .requireAsync(props) .then(loadedModule => { - const result = resolve(loadedModule, { Loadable }) + const result = resolve(loadedModule, this.props, Loadable) if (options.suspense) { this.setCache(result) } this.safeSetState( { - result: resolve(loadedModule, { Loadable }), + result: resolve(loadedModule, this.props, Loadable), loading: false, }, () => this.triggerOnLoad(), diff --git a/packages/component/src/loadable.js b/packages/component/src/loadable.js index 8a407a6d7..6944bf14a 100644 --- a/packages/component/src/loadable.js +++ b/packages/component/src/loadable.js @@ -1,10 +1,10 @@ /* eslint-disable no-use-before-define, react/no-multi-comp */ import React from 'react' import createLoadable from './createLoadable' -import { resolveComponent } from './resolvers' +import { defaultResolveComponent } from './resolvers' export const { loadable, lazy } = createLoadable({ - resolve: resolveComponent, + defaultResolveComponent, render({ result: Component, props }) { return }, diff --git a/packages/component/src/loadable.test.js b/packages/component/src/loadable.test.js index a48083e84..ac3376f1d 100644 --- a/packages/component/src/loadable.test.js +++ b/packages/component/src/loadable.test.js @@ -90,7 +90,7 @@ describe('#loadable', () => { expect(load).toHaveBeenCalledTimes(2) }) - it('supports non-default export', async () => { + it('supports commonjs default export', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() @@ -98,6 +98,22 @@ describe('#loadable', () => { await wait(() => expect(container).toHaveTextContent('loaded')) }) + it('supports non-default export via resolveComponent', async () => { + const load = createLoadFunction() + const importedModule = { exported: () => 'loaded'}; + const resolveComponent = jest.fn(({ exported: component }) => component); + const Component = loadable(load, { + resolveComponent, + }) + const { container } = render() + load.resolve(importedModule) + await wait(() => expect(container).toHaveTextContent('loaded')) + expect(resolveComponent).toHaveBeenCalledWith( + importedModule, + { someProp: '123', __chunkExtractor: undefined, forwardedRef: null }, + ) + }) + it('forwards props', async () => { const load = createLoadFunction() const Component = loadable(load) diff --git a/packages/component/src/resolvers.js b/packages/component/src/resolvers.js index e64a5e24a..cd2728fe1 100644 --- a/packages/component/src/resolvers.js +++ b/packages/component/src/resolvers.js @@ -1,12 +1,6 @@ -import hoistNonReactStatics from 'hoist-non-react-statics' - -export function resolveComponent(loadedModule, { Loadable }) { +export function defaultResolveComponent(loadedModule) { // eslint-disable-next-line no-underscore-dangle - const Component = loadedModule.__esModule - ? loadedModule.default - : loadedModule.default || loadedModule - hoistNonReactStatics(Loadable, Component, { - preload: true, - }) - return Component + return loadedModule.__esModule + ? loadedModule.default + : loadedModule.default || loadedModule } diff --git a/website/src/pages/docs/api-loadable-component.mdx b/website/src/pages/docs/api-loadable-component.mdx index 4412bbc25..906268a7e 100644 --- a/website/src/pages/docs/api-loadable-component.mdx +++ b/website/src/pages/docs/api-loadable-component.mdx @@ -10,13 +10,14 @@ order: 10 Create a loadable component. -| Arguments | Description | -| ------------------ | -------------------------------------------------------------------- | -| `loadFn` | The function call to load the component. | -| `options` | Optional options. | -| `options.fallback` | Fallback displayed during the loading. | -| `options.ssr` | If `false`, it will not be processed server-side. Default to `true`. | -| `options.cacheKey` | Cache key function (see [dynamic import](/docs/dynamic-import/)) | +| Arguments | Description | +| -------------------------- | -------------------------------------------------------------------- | +| `loadFn` | The function call to load the component. | +| `options` | Optional options. | +| `options.resolveComponent` | Function to resolve the imported component from the imported module. | +| `options.fallback` | Fallback displayed during the loading. | +| `options.ssr` | If `false`, it will not be processed server-side. Default to `true`. | +| `options.cacheKey` | Cache key function (see [dynamic import](/docs/dynamic-import/)) | ```js import loadable from '@loadable/component' @@ -24,6 +25,41 @@ import loadable from '@loadable/component' const OtherComponent = loadable(() => import('./OtherComponent')) ``` +### `options.resolveComponent` +This is a function that receives the imported module (what the `import()` call resolves to) and the props, and returns the component. + +The default value assumes that the component is exported as a default export. +It can be customized to make a loadable component where the imported component is not the default export, or even where a different export is chosen depending on the props. +For example: + +```js +// components.js + +export const Apple = () => 'Apple!' +export const Orange = () => 'Orange!' +``` + +```js +// loadable.js + +const LoadableApple = loadable(() => import('./components'), { + resolveComponent: (components) => components.Apple, +}) + +const LoadableOrange = loadable(() => import('./components'), { + resolveComponent: (components) => components.Orange, +}) + +const LoadableFruit = loadable(() => import('./components'), { + resolveComponent: (components, props) => components[props.fruit], +}) + +``` + +**Note:** The default `resolveComponent` breaks Typescript type inference due to CommonJS compatibility. +To avoid this, you can specify `resolveComponent` as `(imported) => imported.default`. +This requires that the imported components have ES6/Harmony exports. + ## lazy Create a loadable component "Suspense" ready. @@ -89,13 +125,14 @@ OtherComponent.load().then(() => { Create a loadable library. -| Arguments | Description | -| ------------------ | -------------------------------------------------------------------- | -| `loadFn` | The function call to load the component. | -| `options` | Optional options. | -| `options.fallback` | Fallback displayed during the loading. | -| `options.ssr` | If `false`, it will not be processed server-side. Default to `true`. | -| `options.cacheKey` | Cache key function (see [dynamic import](/docs/dynamic-import)) | +| Arguments | Description | +| -------------------------- | -------------------------------------------------------------------- | +| `loadFn` | The function call to load the component. | +| `options` | Optional options. | +| `options.resolveComponent` | Function to resolve the imported component from the imported module. | +| `options.fallback` | Fallback displayed during the loading. | +| `options.ssr` | If `false`, it will not be processed server-side. Default to `true`. | +| `options.cacheKey` | Cache key function (see [dynamic import](/docs/dynamic-import)) | ```js import loadable from '@loadable/component' diff --git a/yarn.lock b/yarn.lock index 866727dcb..02369ebbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7938,7 +7938,7 @@ react-dom@^16.12.0: prop-types "^15.6.2" scheduler "^0.18.0" -react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: +react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==