Skip to content

Add a method to retry resolution and pass it to error component #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The asynchronous component factory. Config goes in, an asynchronous component co
- `config` (_Object_) : The configuration object for the async Component. It has the following properties available:
- `resolve` (_() => Promise<Component>_) : A function that should return a `Promise` that will resolve the Component you wish to be async.
- `LoadingComponent` (_Component_, Optional, default: `null`) : A Component that will be displayed until your async Component is resolved. All props will be passed to it.
- `ErrorComponent` (_Component_, Optional, default: `null`) : A Component that will be displayed if any error occurred whilst trying to resolve your component. All props will be passed to it as well as an `error` prop containing the `Error`.
- `ErrorComponent` (_Component_, Optional, default: `null`) : A Component that will be displayed if any error occurred whilst trying to resolve your component. All props will be passed to it as well as an `error` prop containing the `Error` and a `retry` prop that's a function that'll re-attempt calling the `resolve` function.
- `name` (_String_, Optional, default: `'AsyncComponent'`) : Use this if you would like to name the created async Component, which helps when firing up the React Dev Tools for example.
- `autoResolveES2015Default` (_Boolean_, Optional, default: `true`) : Especially useful if you are resolving ES2015 modules. The resolved module will be checked to see if it has a `.default` and if so then the value attached to `.default` will be used. So easy to forget to do that. 😀
- `env` (_String_, Optional) : Provide either `'node'` or `'browser'` so you can write your own environment detection. Especially useful when using PhantomJS or ElectronJS to prerender the React App.
Expand Down
16 changes: 15 additions & 1 deletion commonjs/asyncComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ function asyncComponent(config) {
return undefined;
});
}
}, {
key: 'retryResolvingModule',
value: function retryResolvingModule() {
// clear existing errors
this.registerErrorState(null);
sharedState.error = null;
// clear resolver so it'll be retried
sharedState.resolver = null;
this.resolveModule();
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
Expand All @@ -224,12 +234,16 @@ function asyncComponent(config) {
}, {
key: 'render',
value: function render() {
var _this5 = this;

var _state = this.state,
module = _state.module,
error = _state.error;

if (error) {
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { retry: function retry() {
return _this5.retryResolvingModule();
}, error: error })) : null;
}

// This is as workaround for React Hot Loader support. When using
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/__snapshots__/asyncComponent.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`asyncComponent in a browser environment when an error occurs resolving a component can retry resolving 1`] = `"<div>failed to resolve</div>"`;

exports[`asyncComponent in a browser environment when an error occurs resolving a component can retry resolving 2`] = `"<h1>I loaded now!</h1>"`;

exports[`asyncComponent in a browser environment when an error occurs resolving a component should render the ErrorComponent 1`] = `"<div>failed to resolve</div>"`;

exports[`asyncComponent in a server environment when an error occurs resolving a component should not render the ErrorComponent 1`] = `null`;
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/integration.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ exports[`integration tests browser rendering renders the ErrorComponent 2`] = `
<AsyncComponent>
<ErrorComponent
error={[Error: An error occurred]}
retry={[Function]}
>
<div>
An error occurred
Expand Down Expand Up @@ -194,6 +195,7 @@ exports[`integration tests render server and client 4`] = `
<ErrorAsyncComponent>
<ErrorComponent
error={[Error: This always errors]}
retry={[Function]}
>
<div>
This always errors
Expand Down
42 changes: 41 additions & 1 deletion src/__tests__/asyncComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('asyncComponent', () => {

describe('in a browser environment', () => {
describe('when an error occurs resolving a component', () => {
it.only('should render the ErrorComponent', async () => {
it('should render the ErrorComponent', async () => {
const Bob = asyncComponent({
resolve: () => Promise.reject(new Error('failed to resolve')),
ErrorComponent: ({ error }) => <div>{error.message}</div>,
Expand All @@ -34,12 +34,49 @@ describe('asyncComponent', () => {
await new Promise(resolve => setTimeout(resolve, errorResolveDelay))
expect(renderWrapper.html()).toMatchSnapshot()
})

it('can retry resolving', async () => {
class RetryingError extends React.Component {
componentDidMount() {
setTimeout(() => this.props.retry(), 1)
}
render() {
return <div>{this.props.error.message}</div>
}
}
const asyncProps = {
resolve: jest.fn(() =>
Promise.reject(new Error('failed to resolve')),
),
ErrorComponent: RetryingError,
env: 'browser',
}
const Bob = asyncComponent(asyncProps)
const renderWrapper = mount(<Bob />)

asyncProps.resolve.mockImplementation(() =>
Promise.resolve(() => <h1>I loaded now!</h1>),
)

await new Promise(resolve =>
setTimeout(() => {
expect(renderWrapper.html()).toMatchSnapshot()
setTimeout(() => {
expect(renderWrapper.html()).toMatchSnapshot()
resolve()
}, errorResolveDelay)
}, errorResolveDelay),
)
})
})
})

describe('in a server environment', () => {
describe('when an error occurs resolving a component', () => {
it('should not render the ErrorComponent', async () => {
const consoleSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => true)
const Bob = asyncComponent({
resolve: () => Promise.reject(new Error('failed to resolve')),
ErrorComponent: ({ error }) => <div>{error.message}</div>,
Expand All @@ -48,6 +85,9 @@ describe('asyncComponent', () => {
const renderWrapper = mount(<Bob />)
await new Promise(resolve => setTimeout(resolve, errorResolveDelay))
expect(renderWrapper.html()).toMatchSnapshot()
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to resolve asyncComponent',
)
})
})
})
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ const LoadingComponent = () => <div>Loading...</div>
const errorResolveDelay = 20

describe('integration tests', () => {
let consoleSpy;

beforeEach(() => {
consoleSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => true)
})
it('render server and client', () => {
// we have to delete the window to emulate a server only environment
let windowTemp = global.window
Expand Down Expand Up @@ -233,6 +240,7 @@ describe('integration tests', () => {
<Foo />
</AsyncComponentProvider>
)
expect(consoleSpy).toHaveBeenCalledWith('Failed to resolve asyncComponent')
const bootstrappedApp = await asyncBootstrapper(app)
await new Promise(resolve => setTimeout(resolve, errorResolveDelay))
expect(renderToStaticMarkup(bootstrappedApp)).toMatchSnapshot()
Expand Down
15 changes: 14 additions & 1 deletion src/asyncComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ function asyncComponent(config) {
})
}

retryResolvingModule() {
// clear existing errors
this.registerErrorState(null)
sharedState.error = null
// clear resolver so it'll be retried
sharedState.resolver = null
this.resolveModule()
}

componentWillUnmount() {
this.unmounted = true
}
Expand All @@ -188,7 +197,11 @@ function asyncComponent(config) {
const { module, error } = this.state
if (error) {
return ErrorComponent ? (
<ErrorComponent {...this.props} error={error} />
<ErrorComponent
{...this.props}
retry={() => this.retryResolvingModule()}
error={error}
/>
) : null
}

Expand Down
16 changes: 15 additions & 1 deletion umd/react-async-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,16 @@ function asyncComponent(config) {
return undefined;
});
}
}, {
key: 'retryResolvingModule',
value: function retryResolvingModule() {
// clear existing errors
this.registerErrorState(null);
sharedState.error = null;
// clear resolver so it'll be retried
sharedState.resolver = null;
this.resolveModule();
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
Expand All @@ -485,12 +495,16 @@ function asyncComponent(config) {
}, {
key: 'render',
value: function render() {
var _this5 = this;

var _state = this.state,
module = _state.module,
error = _state.error;

if (error) {
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { retry: function retry() {
return _this5.retryResolvingModule();
}, error: error })) : null;
}

// This is as workaround for React Hot Loader support. When using
Expand Down
2 changes: 1 addition & 1 deletion umd/react-async-component.min.js

Large diffs are not rendered by default.