Skip to content
Merged
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
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ This somewhat helpful and descriptive message is supposed to help you identify
potential problems implementing `observers` early on. If you miss the exception
for some reason and ends up in production (prone to happen with dynamic
children), this component will NOT unmount. Instead, it will gracefully catch
the error so that you can do custom logging and report it. For example:
the error and re-render the children so that you can do custom logging and
report it. For example:

```js
import { Config } from '@researchgate/react-intersection-observer';
Expand All @@ -252,11 +253,16 @@ Config.errorReporter(function(error) {
});
```

If this error happens during mount, it's easy to spot. However, a lot of these
errors usually happen during tree updates, because some child component that was
previously observed suddently ceaces to exist in the UI. This usually means that
either you shouldn't have rendered an `<Observer>` around it anymore or, you
should have used the `disabled` property.
While sometimes this error happens during mount, and it's easy to spot, often
types of errors happen during tree updates, because some child component that
was previously observed suddently ceaces to exist in the UI. This usually means
that either you shouldn't have rendered an `<Observer>` around it anymore or,
you should have used the `disabled` property. That's why we capture errors and
do re-rendering of the children as a fallback.

If another kind of error happens, the `errorReporter` won't be invoked, and by
rendering the children the error will bubble up to the nearest error boundary
you defined.

At [ResearchGate](www.researchgate.net), we have found that not unmounting the
tree just because we failed to `observe()` a DOM node suits our use cases
Expand Down
23 changes: 18 additions & 5 deletions src/IntersectionObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import Config from './config';
const observerOptions = ['root', 'rootMargin', 'threshold'];
const observableProps = ['root', 'rootMargin', 'threshold', 'disabled'];
const { hasOwnProperty, toString } = Object.prototype;
const missingNodeError = new Error(
"ReactIntersectionObserver: Can't find DOM node in the provided children. Make sure to render at least one DOM node in the tree."
);

const getOptions = (props) => {
return observerOptions.reduce((options, key) => {
Expand Down Expand Up @@ -110,9 +113,7 @@ class IntersectionObserver extends React.Component {
return false;
}
if (!this.targetNode) {
throw new Error(
"ReactIntersectionObserver: Can't find DOM node in the provided children. Make sure to render at least one DOM node in the tree."
);
throw missingNodeError;
}
this.observer = createObserver(getOptions(this.props));
this.target = this.targetNode;
Expand Down Expand Up @@ -190,15 +191,27 @@ class ErrorBoundary extends React.Component {
forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
};

static getDerivedStateFromError() {
return { hasError: true };
}

state = {
hasError: false,
};

componentDidCatch(error, info) {
if (Config.errorReporter) {
Config.errorReporter(error, info);
if (error === missingNodeError) {
Config.errorReporter && Config.errorReporter(error, info);
}
}

render() {
const { forwardedRef, ...props } = this.props;

if (this.state.hasError) {
return props.children;
}

return <IntersectionObserver ref={forwardedRef} {...props} />;
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/IntersectionObserver.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,45 @@ test('reports errors by re-throwing trying observer children without a DOM node'
Config.errorReporter = originalErrorReporter;
});

test('render a fallback when some unexpected error happens', () => {
global.spyOn(console, 'error'); // suppress error boundary warning
const originalErrorReporter = Config.errorReporter;
const spy = jest.fn();
Config.errorReporter = spy;
class TestErrorBoundary extends React.Component {
state = { hasError: false };

componentDidCatch() {
this.setState({ hasError: true });
}

render() {
// eslint-disable-next-line react/prop-types
return this.state.hasError ? 'has-error' : this.props.children;
}
}

const Boom = () => {
throw new Error('unexpected rendering error');
};

const children = renderer
.create(
<TestErrorBoundary>
<GuardedIntersectionObserver onChange={noop}>
<Boom />
</GuardedIntersectionObserver>
</TestErrorBoundary>
)
.toJSON();

// Tree changed because of the custom error boundary
expect(children).toBe('has-error');
expect(spy).not.toBeCalled();

Config.errorReporter = originalErrorReporter;
});

test('error boundary forwards ref', () => {
let observer;
renderer.create(
Expand Down