Loading...
;
+ }
+
+ let Bar = loaded.Bar.default;
+ let i18n = loaded.i18n;
+ return ;
+ },
+});
+```
+
+When using `Loadable.Map` the `render(state, props)` method is passed `state` and `props` arguments, and is invoked
+for every render of the Loadable regardless of the internal loadable state.
+
### `Loadable` and `Loadable.Map` Options
@@ -725,17 +803,15 @@ When using with `Loadable.Map` you'll also need to pass a
A [`LoadingComponent`](#loadingcomponent) that renders while a module is
loading or when it errors.
-```js
-Loadable({
- loading: LoadingComponent,
-});
-```
-
-This option is required, if you don't want to render anything, return `null`.
+The `loading` prop changes the arguments received by the `render` method, this is to maintain backwards compatibility.
+ * When the `loading` prop **is** defined, the first argument received by the `render(loaded, props)` method
+ is the `loaded` that is the resolved value of [`opts.loader`](#optsloader)
+ * When the `loading` props **is not** defined, the first argument received by `render(state, props)` method
+ is the loadable state. You can access the loaded component(s) using `state.loaded`.
```js
Loadable({
- loading: () => null,
+ loading: LoadingComponent,
});
```
@@ -769,15 +845,81 @@ Loadable({
#### `opts.render`
-A function to customize the rendering of loaded modules.
+A react element or function to customize the rendering of loaded modules.
+
+##### React element with a `loading` component
-Receives `loaded` which is the resolved value of [`opts.loader`](#optsloader)
+Receives a `loaded` prop that is the resolved value of [`opts.loader`](#optsloader)
and `props` which are the props passed to the
[`LoadableComponent`](#loadablecomponent).
```js
+function CodeSplitRenderer(props) {
+ const { codeSplit, ...componentProps } = props;
+
+ // when used with a 'loading' component, the loaded state is assigned directly to the `codeSplit` prop
+ let Component = codeSplit.default;
+ return ;
+}
+
Loadable({
- render({ loaded }, props) {
+ loading: Loading,
+ render:
+});
+```
+
+##### React element without a `loading` component
+
+Receives `state`, with a `loader` prop that is the resolved value of [`opts.loader`](#optsloader)
+and `props` which are the props passed to the
+[`LoadableComponent`](#loadablecomponent).
+
+```js
+function CodeSplitRenderer(props) {
+ const { codeSplit, ...componentProps } = props;
+
+ // when used without a 'loading' component, the loadable state is assigned to the `codeSplit` prop
+ const { isLoading, loaded, pastDelay, timedOut, error } = codeSplit;
+
+ let Component = loaded.default;
+ return ;
+}
+
+Loadable({
+ render:
+});
+```
+
+##### Function with a `loading` component
+
+Receives a `loaded` prop that is the resolved value of [`opts.loader`](#optsloader)
+and `props` which are the props passed to the
+[`LoadableComponent`](#loadablecomponent).
+
+```js
+Loadable({
+ loading: Loading,
+ render(loaded, props) {
+ let Component = loaded.default;
+ return ;
+ }
+});
+```
+
+##### Function without a `loading` component
+
+Receives `state`, with a `loader` prop that is the resolved value of [`opts.loader`](#optsloader)
+and `props` which are the props passed to the
+[`LoadableComponent`](#loadablecomponent).
+
+```js
+Loadable({
+ render(state, props) {
+ const { isLoading, loaded, pastDelay, timedOut, error } = state;
+ if (isLoading) {
+ return Loading...
;
+ }
+
let Component = loaded.default;
return ;
}
diff --git a/__tests__/__snapshots__/test.js.snap b/__tests__/__snapshots__/test.js.snap
index 5bb3c06..d2f21ac 100644
--- a/__tests__/__snapshots__/test.js.snap
+++ b/__tests__/__snapshots__/test.js.snap
@@ -28,6 +28,33 @@ exports[`delay and timeout 4`] = `
`;
+exports[`loadable map element success without loading prop 1`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":false,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map element success without loading prop 2`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":true,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map element success without loading prop 3`] = `
+
+
+ MyComponent
+ {"prop":"baz"}
+
+
+ MyComponent
+ {"prop":"baz"}
+
+
+`;
+
exports[`loadable map error 1`] = `
MyLoadingComponent
@@ -49,6 +76,27 @@ exports[`loadable map error 3`] = `
`;
+exports[`loadable map error without loading prop 1`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":false,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map error without loading prop 2`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":true,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map error without loading prop 3`] = `
+
+ MyLoadingComponent
+ {"isLoading":false,"pastDelay":true,"timedOut":false,"error":{},"loaded":{"a":{}}}
+
+`;
+
exports[`loadable map success 1`] = `
MyLoadingComponent
@@ -76,6 +124,33 @@ exports[`loadable map success 3`] = `
`;
+exports[`loadable map success without loading prop 1`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":false,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map success without loading prop 2`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":true,"timedOut":false,"error":null,"loaded":{}}
+
+`;
+
+exports[`loadable map success without loading prop 3`] = `
+
+
+ MyComponent
+ {"prop":"baz"}
+
+
+ MyComponent
+ {"prop":"baz"}
+
+
+`;
+
exports[`loading error 1`] = `
MyLoadingComponent
@@ -202,21 +277,63 @@ exports[`render 3`] = `
`;
-exports[`render loading inline 1`] = `
+exports[`render element 1`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":false,"timedOut":false,"error":null}
+
+`;
+
+exports[`render element 2`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":true,"timedOut":false,"error":null}
+
+`;
+
+exports[`render element 3`] = `
+
+ MyComponent
+ {"prop":"baz"}
+
+`;
+
+exports[`render element without loading prop 1`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":false,"timedOut":false,"error":null,"loaded":null}
+
+`;
+
+exports[`render element without loading prop 2`] = `
+
+ MyLoadingComponent
+ {"isLoading":true,"pastDelay":true,"timedOut":false,"error":null,"loaded":null}
+
+`;
+
+exports[`render element without loading prop 3`] = `
+
+ MyComponent
+ {"prop":"baz"}
+
+`;
+
+exports[`render without loading prop 1`] = `
MyLoadingComponent
{"isLoading":true,"pastDelay":false,"timedOut":false,"error":null,"loaded":null}
`;
-exports[`render loading inline 2`] = `
+exports[`render without loading prop 2`] = `
MyLoadingComponent
{"isLoading":true,"pastDelay":true,"timedOut":false,"error":null,"loaded":null}
`;
-exports[`render loading inline 3`] = `
+exports[`render without loading prop 3`] = `
MyComponent
{"prop":"baz"}
diff --git a/__tests__/test.js b/__tests__/test.js
index e19b260..c1ccd42 100644
--- a/__tests__/test.js
+++ b/__tests__/test.js
@@ -30,6 +30,39 @@ function MyComponent(props) {
return
MyComponent {JSON.stringify(props)}
;
}
+function CodeSplitRenderer({ codeSplit, ...props }) {
+ // Enable component to render with or without a 'loading' component configured
+ const { isLoading, loaded } = typeof codeSplit.isLoading === 'boolean' ? codeSplit : { loaded: codeSplit };
+ if (isLoading) {
+ return
;
+ }
+
+ return
;
+}
+
+function CodeSplitMapRenderer({ codeSplit, ...props }) {
+ const { loaded } = codeSplit;
+ return whenLoaded(codeSplit, () => (
+
+
+
+
+ ));
+}
+
+function whenLoaded(state, loadedComponent) {
+ const { isLoading, error, loaded } = state;
+ if (isLoading || error) {
+ return
MyLoadingComponent {JSON.stringify(state)}
;
+ }
+
+ if (loaded) {
+ return loadedComponent();
+ }
+
+ return null;
+}
+
afterEach(async () => {
try {
await Loadable.preloadAll();
@@ -138,7 +171,7 @@ test('render', async () => {
let LoadableMyComponent = Loadable({
loader: createLoader(400, () => ({ MyComponent })),
loading: MyLoadingComponent,
- render({ loaded }, props) {
+ render(loaded, props) {
return
;
}
});
@@ -150,7 +183,21 @@ test('render', async () => {
expect(component.toJSON()).toMatchSnapshot(); // success
});
-test('render loading inline', async () => {
+test('render element', async () => {
+ let LoadableMyComponent = Loadable({
+ loader: createLoader(400, () => ({ MyComponent })),
+ loading: MyLoadingComponent,
+ render:
+ });
+ let component = renderer.create(
);
+ expect(component.toJSON()).toMatchSnapshot(); // initial
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // loading
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // success
+});
+
+test('render without loading prop', async () => {
let LoadableMyComponent = Loadable({
loader: createLoader(400, () => ({ MyComponent })),
render(state, props) {
@@ -169,6 +216,19 @@ test('render loading inline', async () => {
expect(component.toJSON()).toMatchSnapshot(); // success
});
+test('render element without loading prop', async () => {
+ let LoadableMyComponent = Loadable({
+ loader: createLoader(400, () => ({ MyComponent })),
+ render:
+ });
+ let component = renderer.create(
);
+ expect(component.toJSON()).toMatchSnapshot(); // initial
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // loading
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // success
+});
+
test('loadable map success', async () => {
let LoadableMyComponent = Loadable.Map({
loader: {
@@ -176,7 +236,7 @@ test('loadable map success', async () => {
b: createLoader(400, () => ({ MyComponent })),
},
loading: MyLoadingComponent,
- render({ loaded }, props) {
+ render(loaded, props) {
return (
@@ -194,6 +254,48 @@ test('loadable map success', async () => {
expect(component.toJSON()).toMatchSnapshot(); // success
});
+test('loadable map success without loading prop', async () => {
+ let LoadableMyComponent = Loadable.Map({
+ loader: {
+ a: createLoader(200, () => ({ MyComponent })),
+ b: createLoader(400, () => ({ MyComponent })),
+ },
+ render(state, props) {
+ const { loaded } = state;
+ return whenLoaded(state, () => (
+
+
+
+
+ ));
+ }
+ });
+
+ let component = renderer.create(
);
+ expect(component.toJSON()).toMatchSnapshot(); // initial
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // loading
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // success
+});
+
+test('loadable map element success without loading prop', async () => {
+ let LoadableMyComponent = Loadable.Map({
+ loader: {
+ a: createLoader(200, () => ({ MyComponent })),
+ b: createLoader(400, () => ({ MyComponent })),
+ },
+ render:
+ });
+
+ let component = renderer.create(
);
+ expect(component.toJSON()).toMatchSnapshot(); // initial
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // loading
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // success
+});
+
test('loadable map error', async () => {
let LoadableMyComponent = Loadable.Map({
loader: {
@@ -201,7 +303,7 @@ test('loadable map error', async () => {
b: createLoader(400, null, new Error('test error')),
},
loading: MyLoadingComponent,
- render({loaded}, props) {
+ render(loaded, props) {
return (
@@ -216,7 +318,32 @@ test('loadable map error', async () => {
await waitFor(200);
expect(component.toJSON()).toMatchSnapshot(); // loading
await waitFor(200);
- expect(component.toJSON()).toMatchSnapshot(); // success
+ expect(component.toJSON()).toMatchSnapshot(); // error
+});
+
+test('loadable map error without loading prop', async () => {
+ let LoadableMyComponent = Loadable.Map({
+ loader: {
+ a: createLoader(200, () => ({ MyComponent })),
+ b: createLoader(400, null, new Error('test error')),
+ },
+ render(state, props) {
+ const { loaded } = state;
+ return whenLoaded(state, () => (
+
+
+
+
+ ));
+ }
+ });
+
+ let component = renderer.create(
);
+ expect(component.toJSON()).toMatchSnapshot(); // initial
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // loading
+ await waitFor(200);
+ expect(component.toJSON()).toMatchSnapshot(); // error
});
describe('preloadReady', () => {
diff --git a/example/components/PreLoadButton.js b/example/components/PreLoadButton.js
index 42c3f8b..7e744e2 100644
--- a/example/components/PreLoadButton.js
+++ b/example/components/PreLoadButton.js
@@ -3,7 +3,7 @@ import Loadable from 'react-loadable';
import Loading from "./Loading";
const LoadableContent = Loadable({
- loader: () => import('./PreLoadedContent'),
+ loader: () => import(/* webpackChunkName: "PreLoadedContent" */ './PreLoadedContent'),
loading: Loading,
});
diff --git a/src/index.js b/src/index.js
index 696b180..67dc854 100644
--- a/src/index.js
+++ b/src/index.js
@@ -87,7 +87,15 @@ function resolve(obj) {
return obj && obj.__esModule ? obj.default : obj;
}
-function defaultRender(state, props) {
+function backwardsCompatibleRender(loaded, props) {
+ if (loaded) {
+ return React.createElement(resolve(loaded), props);
+ }
+
+ return null;
+}
+
+function stateRender(state, props) {
const { loaded } = state;
if (loaded) {
return React.createElement(resolve(loaded), props);
@@ -102,7 +110,7 @@ function createLoadableComponent(loadFn, options) {
loading: null,
delay: 200,
timeout: null,
- render: defaultRender,
+ render: options.loading ? backwardsCompatibleRender : stateRender,
webpack: null,
modules: [],
}, options);
@@ -227,14 +235,20 @@ function createLoadableComponent(loadFn, options) {
};
if (opts.loading) {
- // maintain backwards compatibility - support 'loading' option
- if ((renderState.isLoading || renderState.error) && opts.loading) {
- return React.createElement(opts.loading, renderState)
+ // maintain full backwards compatibility - support 'loading' option
+ if (renderState.isLoading || renderState.error) {
+ return React.createElement(opts.loading, renderState);
}
+
+ return React.isValidElement(opts.render) ?
+ React.cloneElement(opts.render, Object.assign({}, this.props, { codeSplit: this.state.loaded })) :
+ opts.render(this.state.loaded, this.props);
}
renderState.loaded = this.state.loaded;
- return opts.render(renderState, this.props);
+ return React.isValidElement(opts.render) ?
+ React.cloneElement(opts.render, Object.assign({}, this.props, { codeSplit: renderState })) :
+ opts.render(renderState, this.props);
}
};
}
@@ -244,8 +258,8 @@ function Loadable(opts) {
}
function LoadableMap(opts) {
- if (typeof opts.render !== 'function') {
- throw new Error('LoadableMap requires a `render(state, props)` function');
+ if (!(React.isValidElement(opts.render) || typeof opts.render === 'function')) {
+ throw new Error('LoadableMap requires a `render` react element or function');
}
return createLoadableComponent(loadMap, opts);