Skip to content

Commit a96c319

Browse files
committed
fix(boundary): DOM missing throws; no error boundary
1 parent 3bc825b commit a96c319

File tree

6 files changed

+14
-269
lines changed

6 files changed

+14
-269
lines changed

README.md

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ all the imperative parts for you.
3232
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
3333
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3434

35+
3536
- [Getting started](#getting-started)
3637
- [What does IntersectionObserver do?](#what-does-intersectionobserver-do)
3738
- [Why use this component?](#why-use-this-component)
@@ -41,7 +42,7 @@ all the imperative parts for you.
4142
- [Documentation](#documentation)
4243
- [Demos](#demos)
4344
- [Recipes](#recipes)
44-
- [Handling a missing DOM node situation](#handling-a-missing-dom-node-situation)
45+
- [Missing DOM nodes when observing](#missing-dom-nodes-when-observing)
4546
- [Options](#options)
4647
- [Notes](#notes)
4748
- [Polyfill](#polyfill)
@@ -215,7 +216,7 @@ export default () => (
215216

216217
Discover more recipes in our [examples section](docs/README.md).
217218

218-
### Handling a missing DOM node situation
219+
### Missing DOM nodes when observing
219220

220221
In cases where there isn't a DOM node available to observe when rendering,
221222
you'll be seeing an error logged in the console:
@@ -227,49 +228,12 @@ ReactIntersectionObserver: Can't find DOM node in the provided children. Make su
227228
This somewhat helpful and descriptive message is supposed to help you identify
228229
potential problems implementing `observers` early on. If you miss the exception
229230
for some reason and ends up in production (prone to happen with dynamic
230-
children), this component will NOT unmount. Instead, it will gracefully catch
231-
the error and re-render the children so that you can do custom logging and
232-
report it. For example:
233-
234-
```js
235-
import { Config } from '@researchgate/react-intersection-observer';
236-
237-
if (process.env.NODE_ENV === 'production') {
238-
Config.errorReporter(function(error) {
239-
sendReport(error);
240-
});
241-
}
242-
```
243-
244-
Maybe you want to deal with the error on your own, for example, by rendering a
245-
fallback. In that case, you can re-throw the error so that it bubbles up to the
246-
next boundary:
247-
248-
```js
249-
import { Config } from '@researchgate/react-intersection-observer';
250-
251-
Config.errorReporter(function(error) {
252-
throw error;
253-
});
254-
```
231+
children), the entire tree will unmount so be sensible about placing your error
232+
boundaries.
255233
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.
266-
267-
At [ResearchGate](www.researchgate.net), we have found that not unmounting the
268-
tree just because we failed to `observe()` a DOM node suits our use cases
269-
better. It's fairly common having a lack of error boundaries around your
270-
components, and that leads to entire UIs parts being unmounted, which is not
271-
ideal to end users. By capturing errors, we are able to keep the UI unbroken
272-
while we fix them.
234+
Ultimately the way to avoid this is to either make sure you are rendering a DOM
235+
node inside your `<Observer>`, or to disable the observer until there's one
236+
`<Observer disabled>`.
273237

274238
### Options
275239

src/IntersectionObserver.js

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ import { findDOMNode } from 'react-dom';
33
import PropTypes from 'prop-types';
44
import { createObserver, observeElement, unobserveElement } from './observer';
55
import { shallowCompare } from './utils';
6-
import Config from './config';
76

87
const observerOptions = ['root', 'rootMargin', 'threshold'];
98
const observableProps = ['root', 'rootMargin', 'threshold', 'disabled'];
109
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-
);
1410

15-
const getOptions = (props) => {
11+
export const getOptions = (props) => {
1612
return observerOptions.reduce((options, key) => {
1713
if (hasOwnProperty.call(props, key)) {
1814
const rootIsString =
@@ -26,7 +22,7 @@ const getOptions = (props) => {
2622
}, {});
2723
};
2824

29-
class IntersectionObserver extends React.Component {
25+
export default class IntersectionObserver extends React.Component {
3026
static displayName = 'IntersectionObserver';
3127

3228
static propTypes = {
@@ -113,7 +109,9 @@ class IntersectionObserver extends React.Component {
113109
return false;
114110
}
115111
if (!this.targetNode) {
116-
throw missingNodeError;
112+
throw new Error(
113+
"ReactIntersectionObserver: Can't find DOM node in the provided children. Make sure to render at least one DOM node in the tree."
114+
);
117115
}
118116
this.observer = createObserver(getOptions(this.props));
119117
this.target = this.targetNode;
@@ -183,47 +181,3 @@ class IntersectionObserver extends React.Component {
183181
: null;
184182
}
185183
}
186-
187-
class ErrorBoundary extends React.Component {
188-
static displayName = 'ErrorBoundary(IntersectionObserver)';
189-
190-
static propTypes = {
191-
forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
192-
};
193-
194-
static getDerivedStateFromError() {
195-
return { hasError: true };
196-
}
197-
198-
state = {
199-
hasError: false,
200-
};
201-
202-
componentDidCatch(error, info) {
203-
if (error === missingNodeError) {
204-
Config.errorReporter && Config.errorReporter(error, info);
205-
}
206-
}
207-
208-
render() {
209-
const { forwardedRef, ...props } = this.props;
210-
211-
if (this.state.hasError) {
212-
return props.children;
213-
}
214-
215-
return <IntersectionObserver ref={forwardedRef} {...props} />;
216-
}
217-
}
218-
219-
const GuardedIntersectionObserver = React.forwardRef((props, ref) => (
220-
<ErrorBoundary forwardedRef={ref} {...props} />
221-
));
222-
223-
GuardedIntersectionObserver.displayName = 'IntersectionObserver';
224-
225-
export {
226-
GuardedIntersectionObserver as default,
227-
IntersectionObserver,
228-
getOptions,
229-
};

src/__tests__/IntersectionObserver.spec.js

Lines changed: 1 addition & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@
22
import 'intersection-observer';
33
import React, { Component } from 'react';
44
import renderer from 'react-test-renderer';
5-
import GuardedIntersectionObserver, {
6-
IntersectionObserver,
7-
getOptions,
8-
} from '../IntersectionObserver';
5+
import IntersectionObserver, { getOptions } from '../IntersectionObserver';
96
import { callback, observerElementsMap } from '../observer';
10-
import Config from '../config';
117

128
jest.mock('react-dom', () => {
139
const { findDOMNode } = jest.requireActual('react-dom');
@@ -22,8 +18,6 @@ jest.mock('react-dom', () => {
2218
},
2319
};
2420
});
25-
// the default "undefined" can't be re-assigned, so we preemptively set it as an empty function
26-
Config.errorReporter = function() {};
2721

2822
const target = { nodeType: 1, type: 'span' };
2923
const targets = { div: { nodeType: 1, type: 'div' }, span: target };
@@ -76,127 +70,6 @@ test('throws trying to observe children without a DOM node', () => {
7670
expect(observerElementsMap.size).toBe(sizeBeforeObserving);
7771
});
7872

79-
test('reports error trying to observe children without a DOM node', () => {
80-
global.spyOn(console, 'error'); // suppress error boundary warning
81-
const sizeBeforeObserving = observerElementsMap.size;
82-
const originalErrorReporter = Config.errorReporter;
83-
const spy = jest.fn();
84-
Config.errorReporter = spy;
85-
86-
const tree = renderer
87-
.create(
88-
<GuardedIntersectionObserver onChange={noop}>
89-
<ProxyComponent>{null}</ProxyComponent>
90-
</GuardedIntersectionObserver>
91-
)
92-
.toTree();
93-
94-
expect(observerElementsMap.size).toBe(sizeBeforeObserving);
95-
expect(spy).toBeCalledTimes(1);
96-
expect(spy).toBeCalledWith(
97-
expect.any(Error),
98-
expect.objectContaining({ componentStack: expect.any(String) })
99-
);
100-
// Tree stayed mounted because of the error boundary
101-
expect(tree.props.children.type).toEqual(ProxyComponent);
102-
103-
Config.errorReporter = originalErrorReporter;
104-
});
105-
106-
test('reports errors by re-throwing trying observer children without a DOM node', () => {
107-
global.spyOn(console, 'error'); // suppress error boundary warning
108-
const originalErrorReporter = Config.errorReporter;
109-
let called = false;
110-
Config.errorReporter = (err) => {
111-
called = true;
112-
throw err;
113-
};
114-
class TestErrorBoundary extends React.Component {
115-
state = { hasError: false };
116-
117-
componentDidCatch() {
118-
this.setState({ hasError: true });
119-
}
120-
121-
render() {
122-
// eslint-disable-next-line react/prop-types
123-
return this.state.hasError ? 'has-error' : this.props.children;
124-
}
125-
}
126-
127-
const children = renderer
128-
.create(
129-
<TestErrorBoundary>
130-
<GuardedIntersectionObserver onChange={noop}>
131-
<ProxyComponent>{null}</ProxyComponent>
132-
</GuardedIntersectionObserver>
133-
</TestErrorBoundary>
134-
)
135-
.toJSON();
136-
137-
// Tree changed because of the custom error boundary
138-
expect(children).toBe('has-error');
139-
expect(called).toBe(true);
140-
141-
Config.errorReporter = originalErrorReporter;
142-
});
143-
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-
183-
test('error boundary forwards ref', () => {
184-
let observer;
185-
renderer.create(
186-
<GuardedIntersectionObserver
187-
onChange={noop}
188-
ref={(instance) => {
189-
observer = instance;
190-
}}
191-
>
192-
<div />
193-
</GuardedIntersectionObserver>,
194-
{ createNodeMock }
195-
);
196-
197-
expect(observer instanceof IntersectionObserver).toBe(true);
198-
});
199-
20073
test('should not observe children that equal null or undefined', () => {
20174
const sizeBeforeObserving = observerElementsMap.size;
20275
renderer.create(

src/__tests__/config.spec.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/config.js

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export { default } from './IntersectionObserver';
22
export { parseRootMargin } from './utils';
3-
export { default as Config } from './config';

0 commit comments

Comments
 (0)