Skip to content

Commit 05b6181

Browse files
authored
Simplify the API (#88)
* refactor: rewrite the component This removes `render`, and changes `children` to only accept a function * refactor: accept plain children with deprecation warning * test: test the deprecated methods * test: reset NODE_ENV * fix: spread props to the plain child * style: apply prettier * refactor: log the DOM node in deprecations
1 parent 308705d commit 05b6181

File tree

7 files changed

+132
-196
lines changed

7 files changed

+132
-196
lines changed

README.md

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,54 +35,20 @@ npm install react-intersection-observer --save
3535

3636
### Child as function
3737

38-
The easiest way to use the `Observer`, is to pass a function as the child. It
39-
will be called whenever the state changes, with the new value of `inView`.
40-
By default it will render inside a `<div>`, but you can change the element by setting `tag` to the HTMLElement you need.
38+
To use the `Observer`, you pass it a function. It will be called whenever the state changes, with the new value of `inView`.
39+
In addition to the `inView` prop, children also receives a `ref` that should be set on the containing DOM element.
40+
This is the element that the IntersectionObserver will monitor.
4141

4242
```js
4343
import Observer from 'react-intersection-observer'
4444

4545
const Component = () => (
4646
<Observer>
47-
{inView => <h2>{`Header inside viewport ${inView}.`}</h2>}
48-
</Observer>
49-
)
50-
51-
export default Component
52-
```
53-
54-
### Render prop
55-
56-
Using the render prop you can get full control over the output.
57-
In addition to the `inView` prop, the render also receives a `ref` that should be set on the containing DOM element.
58-
59-
```js
60-
import Observer from 'react-intersection-observer'
61-
62-
const Component = () => (
63-
<Observer
64-
render={({ inView, ref }) => (
47+
{({ inView, ref }) => (
6548
<div ref={ref}>
6649
<h2>{`Header inside viewport ${inView}.`}</h2>
6750
</div>
6851
)}
69-
/>
70-
)
71-
72-
export default Component
73-
```
74-
75-
### OnChange callback
76-
77-
You can monitor the onChange method, and control the state in your own
78-
component. This works with plain children, child as function or render props.
79-
80-
```js
81-
import Observer from 'react-intersection-observer'
82-
83-
const Component = () => (
84-
<Observer onChange={inView => console.log('Inview:', inView)}>
85-
<h2>Plain children are always rendered. Use onChange to monitor state.</h2>
8652
</Observer>
8753
)
8854

@@ -95,15 +61,13 @@ The **`<Observer />`** accepts the following props:
9561

9662
| Name | Type | Default | Required | Description |
9763
| --------------- | ----------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98-
| **children** | Func/Node | | false | Children should be either a function or a node |
99-
| **render** | ({inView, ref}) => Node | | false | Render prop allowing you to control the view. |
64+
| **children** | ({inView, ref}) => Node | | true | Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. |
65+
| **onChange** | (inView) => void | | false | Call this function whenever the in view state changes |
10066
| **root** | HTMLElement | | false | The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null. |
10167
| **rootId** | String | | false | Unique identifier for the root element - This is used to identify the IntersectionObserver instance, so it can be reused. If you defined a root element, without adding an id, it will create a new instance for all components. |
10268
| **rootMargin** | String | '0px' | false | Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). |
103-
| **tag** | String | 'div' | false | Element tag to use for the wrapping element when rendering using 'children'. Defaults to 'div' |
10469
| **threshold** | Number | 0 | false | Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. |
10570
| **triggerOnce** | Bool | false | false | Only trigger this method once |
106-
| **onChange** | Func | | false | Call this function whenever the in view state changes |
10771

10872
## Usage in other projects
10973

index.d.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@ export interface RenderProps {
66
}
77

88
export interface IntersectionObserverProps {
9-
/** Children should be either a function or a node */
10-
children?: React.ReactNode | ((inView: boolean) => React.ReactNode)
11-
12-
/** Render prop boolean indicating inView state */
13-
render?: (fields: RenderProps) => React.ReactNode
9+
/** Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. */
10+
children?: (fields: RenderProps) => React.ReactNode
1411

1512
/**
1613
* The `HTMLElement` that is used as the viewport for checking visibility of
@@ -34,12 +31,6 @@ export interface IntersectionObserverProps {
3431
*/
3532
rootMargin?: string
3633

37-
/**
38-
* Element tag to use for the wrapping component
39-
* @default `'div'`
40-
*/
41-
tag?: string
42-
4334
/** Number between 0 and 1 indicating the the percentage that should be
4435
* visible before triggering. Can also be an array of numbers, to create
4536
* multiple trigger points.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"build:lib": "run-p rollup:*",
3232
"build:storybook": "build-storybook --output-dir example",
3333
"build:flow": "node scripts/create-flow",
34-
"dev": "concurrently -k -r 'jest --watch' 'npm run storybook'",
34+
"dev": "concurrently -k -r 'jest --watch' 'yarn run storybook'",
3535
"lint": "eslint {src,stories,tests}/**/*.js ",
3636
"rollup:es": "rollup -c --environment FORMAT:es",
3737
"rollup:cjs": "rollup -c --environment FORMAT:cjs",

src/index.js

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ import { observe, unobserve } from './intersection'
44
import invariant from 'invariant'
55

66
type Props = {
7-
/** Element tag to use for the wrapping element when rendering using 'children'. Defaults to 'div' */
8-
tag: string,
9-
/** Only trigger the inView callback once */
10-
triggerOnce: boolean,
11-
/** Children should be either a function or a node */
12-
children?: ((inView: boolean) => React.Node) | React.Node,
13-
/** Render prop boolean indicating inView state */
7+
/** Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. */
8+
children?: ({
9+
inView: boolean,
10+
ref: (node: ?HTMLElement) => void,
11+
}) => React.Node,
12+
/** @deprecated replace render with children */
1413
render?: ({
1514
inView: boolean,
1615
ref: (node: ?HTMLElement) => void,
1716
}) => React.Node,
17+
/** @deprecated */
18+
tag?: string,
1819
/** Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. */
1920
threshold?: number | Array<number>,
21+
/** Only trigger the inView callback once */
22+
triggerOnce: boolean,
2023
/** The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null.*/
2124
root?: HTMLElement,
2225
/** Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). */
@@ -36,14 +39,13 @@ type State = {
3639
* Monitors scroll, and triggers the children function with updated props
3740
*
3841
<Observer>
39-
{inView => (
40-
<h1>{`${inView}`}</h1>
42+
{({inView, ref}) => (
43+
<h1 ref={ref}>{`${inView}`}</h1>
4144
)}
4245
</Observer>
4346
*/
4447
class Observer extends React.Component<Props, State> {
4548
static defaultProps = {
46-
tag: 'div',
4749
threshold: 0,
4850
triggerOnce: false,
4951
}
@@ -53,10 +55,21 @@ class Observer extends React.Component<Props, State> {
5355
}
5456

5557
componentDidMount() {
56-
if (typeof this.props.render === 'function') {
58+
if (process.env.NODE_ENV !== 'production') {
59+
if (this.props.hasOwnProperty('render')) {
60+
console.warn(
61+
`react-intersection-observer: "render" is deprecated, and should be replaced with "children"`,
62+
this.node,
63+
)
64+
} else if (typeof this.props.children !== 'function') {
65+
console.warn(
66+
`react-intersection-observer: plain "children" is deprecated. You should convert it to a function that handles the "ref" manually.`,
67+
this.node,
68+
)
69+
}
5770
invariant(
5871
this.node,
59-
`react-intersection-observer: No DOM node found. Make sure you forward "ref" to the root DOM element you want to observe, when using render prop.`,
72+
`react-intersection-observer: No DOM node found. Make sure you forward "ref" to the root DOM element you want to observe.`,
6073
)
6174
}
6275
}
@@ -131,20 +144,16 @@ class Observer extends React.Component<Props, State> {
131144
} = this.props
132145

133146
const { inView } = this.state
147+
const renderMethod = children || render
134148

135-
if (typeof render === 'function') {
136-
return render({ inView, ref: this.handleNode })
149+
if (typeof renderMethod === 'function') {
150+
return renderMethod({ inView, ref: this.handleNode })
137151
}
138152

139153
return React.createElement(
140-
tag,
141-
{
142-
...props,
143-
ref: this.handleNode,
144-
},
145-
// If children is a function, render it with the current inView status.
146-
// Otherwise always render children. Assume onChange is being used outside, to control the the state of children.
147-
typeof children === 'function' ? children(inView) : children,
154+
tag || 'div',
155+
{ ref: this.handleNode, ...props },
156+
children,
148157
)
149158
}
150159
}

stories/Observer.story.js

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import RootComponent from './Root'
99
type Props = {
1010
style?: Object,
1111
children?: React.Node,
12-
innerRef?: Function,
1312
}
1413

15-
const Header = (props: Props) => (
14+
// $FlowFixMe forwardRef is not known
15+
const Header = React.forwardRef((props: Props, ref) => (
1616
<div
17-
ref={props.innerRef}
17+
ref={ref}
1818
style={{
1919
display: 'flex',
2020
minHeight: '25vh',
@@ -29,13 +29,15 @@ const Header = (props: Props) => (
2929
>
3030
<h2>{props.children}</h2>
3131
</div>
32-
)
32+
))
3333

3434
storiesOf('Intersection Observer', module)
35-
.add('Child as function', () => (
35+
.add('Basic', () => (
3636
<ScrollWrapper>
3737
<Observer onChange={action('Child Observer inview')}>
38-
{inView => <Header>Header inside viewport: {inView.toString()}</Header>}
38+
{({ inView, ref }) => (
39+
<Header ref={ref}>Header inside viewport: {inView.toString()}</Header>
40+
)}
3941
</Observer>
4042
</ScrollWrapper>
4143
))
@@ -44,27 +46,18 @@ storiesOf('Intersection Observer', module)
4446
<Observer
4547
onChange={action('Render Observer inview')}
4648
render={({ inView, ref }) => (
47-
<Header innerRef={ref}>
49+
<Header ref={ref}>
4850
Header is inside viewport: {inView.toString()}
4951
</Header>
5052
)}
5153
/>
5254
</ScrollWrapper>
5355
))
54-
.add('Plain child', () => (
55-
<ScrollWrapper>
56-
<Observer onChange={action('Plain Observer inview')}>
57-
<Header>
58-
Plain children are always rendered. Use onChange to monitor state.
59-
</Header>
60-
</Observer>
61-
</ScrollWrapper>
62-
))
6356
.add('Taller then viewport', () => (
6457
<ScrollWrapper>
6558
<Observer onChange={action('Child Observer inview')}>
66-
{inView => (
67-
<Header style={{ height: '150vh' }}>
59+
{({ inView, ref }) => (
60+
<Header ref={ref} style={{ height: '150vh' }}>
6861
Header is inside the viewport: {inView.toString()}
6962
</Header>
7063
)}
@@ -74,8 +67,8 @@ storiesOf('Intersection Observer', module)
7467
.add('With threshold 100%', () => (
7568
<ScrollWrapper>
7669
<Observer threshold={1} onChange={action('Child Observer inview')}>
77-
{inView => (
78-
<Header>
70+
{({ inView, ref }) => (
71+
<Header ref={ref}>
7972
Header is fully inside the viewport: {inView.toString()}
8073
</Header>
8174
)}
@@ -85,8 +78,8 @@ storiesOf('Intersection Observer', module)
8578
.add('With threshold 50%', () => (
8679
<ScrollWrapper>
8780
<Observer threshold={0.5} onChange={action('Child Observer inview')}>
88-
{inView => (
89-
<Header>
81+
{({ inView, ref }) => (
82+
<Header ref={ref}>
9083
Header is 50% inside the viewport: {inView.toString()}
9184
</Header>
9285
)}
@@ -96,8 +89,8 @@ storiesOf('Intersection Observer', module)
9689
.add('Taller then viewport with threshold 100%', () => (
9790
<ScrollWrapper>
9891
<Observer threshold={1}>
99-
{inView => (
100-
<Header style={{ height: '150vh' }}>
92+
{({ inView, ref }) => (
93+
<Header ref={ref} style={{ height: '150vh' }}>
10194
Header is fully inside the viewport: {inView.toString()}
10295
</Header>
10396
)}
@@ -110,8 +103,8 @@ storiesOf('Intersection Observer', module)
110103
threshold={[0, 0.25, 0.5, 0.75, 1]}
111104
onChange={action('Hit threshold trigger')}
112105
>
113-
{inView => (
114-
<Header>
106+
{({ inView, ref }) => (
107+
<Header ref={ref}>
115108
Header is inside threshold: {inView.toString()} - onChange triggers
116109
multiple times.
117110
</Header>
@@ -130,8 +123,8 @@ storiesOf('Intersection Observer', module)
130123
rootId="window1"
131124
onChange={action('Child Observer inview')}
132125
>
133-
{inView => (
134-
<Header>
126+
{({ inView, ref }) => (
127+
<Header ref={ref}>
135128
Header is inside the root viewport: {inView.toString()}
136129
</Header>
137130
)}
@@ -151,8 +144,8 @@ storiesOf('Intersection Observer', module)
151144
rootId="window2"
152145
onChange={action('Child Observer inview')}
153146
>
154-
{inView => (
155-
<Header>
147+
{({ inView, ref }) => (
148+
<Header ref={ref}>
156149
Header is inside the root viewport: {inView.toString()}
157150
</Header>
158151
)}
@@ -168,8 +161,8 @@ storiesOf('Intersection Observer', module)
168161
triggerOnce
169162
onChange={action('Child Observer inview')}
170163
>
171-
{inView => (
172-
<Header>
164+
{({ inView, ref }) => (
165+
<Header ref={ref}>
173166
Header was fully inside the viewport: {inView.toString()}
174167
</Header>
175168
)}
@@ -179,15 +172,15 @@ storiesOf('Intersection Observer', module)
179172
.add('Multiple observers', () => (
180173
<ScrollWrapper>
181174
<Observer threshold={1} onChange={action('Child Observer inview')}>
182-
{inView => (
183-
<Header>
175+
{({ inView, ref }) => (
176+
<Header ref={ref}>
184177
Header 1 is fully inside the viewport: {inView.toString()}
185178
</Header>
186179
)}
187180
</Observer>
188181
<Observer threshold={1} onChange={action('Child Observer inview')}>
189-
{inView => (
190-
<Header>
182+
{({ inView, ref }) => (
183+
<Header ref={ref}>
191184
Header 2 is fully inside the viewport: {inView.toString()}
192185
</Header>
193186
)}

0 commit comments

Comments
 (0)