Skip to content

Commit

Permalink
feat: add resolveComponent option
Browse files Browse the repository at this point in the history
`resolveComponent` is a synchronous function to resolve the react
component from the imported module and props. Unlike wrappers on the
import function itself, it works on both the client and server side.
  • Loading branch information
hedgepigdaniel committed Jan 9, 2020
1 parent 9e00f2e commit a47d3d9
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 35 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion packages/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
23 changes: 19 additions & 4 deletions packages/component/src/createLoadable.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 = {}
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions packages/component/src/loadable.js
Original file line number Diff line number Diff line change
@@ -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 <Component {...props} />
},
Expand Down
18 changes: 17 additions & 1 deletion packages/component/src/loadable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,30 @@ 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(<Component />)
load.resolve(() => 'loaded')
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(<Component someProp="123" />)
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)
Expand Down
14 changes: 4 additions & 10 deletions packages/component/src/resolvers.js
Original file line number Diff line number Diff line change
@@ -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
}
65 changes: 51 additions & 14 deletions website/src/pages/docs/api-loadable-component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,56 @@ 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'

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.
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down

0 comments on commit a47d3d9

Please sign in to comment.