Skip to content

Commit 78eb990

Browse files
authored
fix(errorBoundary): fallback to children to propagate different errors (#105)
1 parent 282e936 commit 78eb990

File tree

3 files changed

+69
-11
lines changed

3 files changed

+69
-11
lines changed

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ This somewhat helpful and descriptive message is supposed to help you identify
228228
potential problems implementing `observers` early on. If you miss the exception
229229
for some reason and ends up in production (prone to happen with dynamic
230230
children), this component will NOT unmount. Instead, it will gracefully catch
231-
the error so that you can do custom logging and report it. For example:
231+
the error and re-render the children so that you can do custom logging and
232+
report it. For example:
232233
233234
```js
234235
import { Config } from '@researchgate/react-intersection-observer';
@@ -252,11 +253,16 @@ Config.errorReporter(function(error) {
252253
});
253254
```
254255
255-
If this error happens during mount, it's easy to spot. However, a lot of these
256-
errors usually happen during tree updates, because some child component that was
257-
previously observed suddently ceaces to exist in the UI. This usually means that
258-
either you shouldn't have rendered an `<Observer>` around it anymore or, you
259-
should have used the `disabled` property.
256+
While sometimes this error happens during mount, and it's easy to spot, often
257+
types of errors happen during tree updates, because some child component that
258+
was previously observed suddently ceaces to exist in the UI. This usually means
259+
that either you shouldn't have rendered an `<Observer>` around it anymore or,
260+
you should have used the `disabled` property. That's why we capture errors and
261+
do re-rendering of the children as a fallback.
262+
263+
If another kind of error happens, the `errorReporter` won't be invoked, and by
264+
rendering the children the error will bubble up to the nearest error boundary
265+
you defined.
260266
261267
At [ResearchGate](www.researchgate.net), we have found that not unmounting the
262268
tree just because we failed to `observe()` a DOM node suits our use cases

src/IntersectionObserver.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import Config from './config';
88
const observerOptions = ['root', 'rootMargin', 'threshold'];
99
const observableProps = ['root', 'rootMargin', 'threshold', 'disabled'];
1010
const { hasOwnProperty, toString } = Object.prototype;
11+
const missingNodeError = new Error(
12+
"ReactIntersectionObserver: Can't find DOM node in the provided children. Make sure to render at least one DOM node in the tree."
13+
);
1114

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

194+
static getDerivedStateFromError() {
195+
return { hasError: true };
196+
}
197+
198+
state = {
199+
hasError: false,
200+
};
201+
193202
componentDidCatch(error, info) {
194-
if (Config.errorReporter) {
195-
Config.errorReporter(error, info);
203+
if (error === missingNodeError) {
204+
Config.errorReporter && Config.errorReporter(error, info);
196205
}
197206
}
198207

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

211+
if (this.state.hasError) {
212+
return props.children;
213+
}
214+
202215
return <IntersectionObserver ref={forwardedRef} {...props} />;
203216
}
204217
}

src/__tests__/IntersectionObserver.spec.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,45 @@ test('reports errors by re-throwing trying observer children without a DOM node'
141141
Config.errorReporter = originalErrorReporter;
142142
});
143143

144+
test('render a fallback when some unexpected error happens', () => {
145+
global.spyOn(console, 'error'); // suppress error boundary warning
146+
const originalErrorReporter = Config.errorReporter;
147+
const spy = jest.fn();
148+
Config.errorReporter = spy;
149+
class TestErrorBoundary extends React.Component {
150+
state = { hasError: false };
151+
152+
componentDidCatch() {
153+
this.setState({ hasError: true });
154+
}
155+
156+
render() {
157+
// eslint-disable-next-line react/prop-types
158+
return this.state.hasError ? 'has-error' : this.props.children;
159+
}
160+
}
161+
162+
const Boom = () => {
163+
throw new Error('unexpected rendering error');
164+
};
165+
166+
const children = renderer
167+
.create(
168+
<TestErrorBoundary>
169+
<GuardedIntersectionObserver onChange={noop}>
170+
<Boom />
171+
</GuardedIntersectionObserver>
172+
</TestErrorBoundary>
173+
)
174+
.toJSON();
175+
176+
// Tree changed because of the custom error boundary
177+
expect(children).toBe('has-error');
178+
expect(spy).not.toBeCalled();
179+
180+
Config.errorReporter = originalErrorReporter;
181+
});
182+
144183
test('error boundary forwards ref', () => {
145184
let observer;
146185
renderer.create(

0 commit comments

Comments
 (0)