Skip to content

Commit 49e43cc

Browse files
committed
Refactor the render method so it accepts ref, allowing full control over the output
1 parent bfd280f commit 49e43cc

File tree

9 files changed

+2063
-1812
lines changed

9 files changed

+2063
-1812
lines changed

README.md

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,81 @@ or NPM:
4747
npm install react-intersection-observer --save
4848
```
4949

50+
## Props
51+
52+
The **`<Observer />`** accepts the following props:
53+
54+
| Name | Type | Default | Required | Description |
55+
| --------------- | ----------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56+
| **children** | Func/Node | | false | Children should be either a function or a node |
57+
| **render** | ({inView, ref}) => Node | | false | Render prop allowing you to control the view. |
58+
| **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. |
59+
| **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. |
60+
| **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). |
61+
| **tag** | String | 'div' | false | Element tag to use for the wrapping component |
62+
| **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. |
63+
| **triggerOnce** | Bool | false | false | Only trigger this method once |
64+
| **onChange** | Func | | false | Call this function whenever the in view state changes |
65+
66+
## Examples
67+
68+
### Child as function
69+
70+
The default way to use the `Observer`, is to pass a function as the child. It
71+
will be called whenever the state changes, with the new value of `inView`.
72+
By default it will render inside a `<div>`, but you can change the element by setting `tag` to the HTMLElement you need.
73+
74+
```js
75+
import Observer from 'react-intersection-observer'
76+
77+
const Component = () => (
78+
<Observer>
79+
{inView => <h2>{`Header inside viewport ${inView}.`}</h2>}
80+
</Observer>
81+
)
82+
83+
export default Component
84+
```
85+
86+
### Render prop
87+
88+
Using the render prop you can get full control over the output.
89+
In addition to the `inView` prop, the render also receives a `ref` that should be set on the containing DOM element.
90+
91+
```js
92+
import Observer from 'react-intersection-observer'
93+
94+
const Component = () => (
95+
<Observer
96+
render={({ inView, ref }) => (
97+
<div ref={ref}>
98+
<h2>{`Header inside viewport ${inView}.`}</h2>
99+
</div>
100+
)}
101+
/>
102+
)
103+
104+
export default Component
105+
```
106+
107+
### OnChange callback
108+
109+
You can monitor the onChange method, and control the state in your own
110+
component. The child node will always be rendered.
111+
112+
```js
113+
import Observer from 'react-intersection-observer'
114+
115+
const Component = () => (
116+
<Observer onChange={inView => console.log('Inview:', inView)}>
117+
<h2>Plain children are always rendered. Use onChange to monitor state.</h2>
118+
</Observer>
119+
)
120+
121+
export default Component
122+
```
123+
124+
50125
### Polyfill for intersection-observer
51126

52127
The component requires the [intersection-observer
@@ -97,69 +172,4 @@ function supportsIntersectionObserver() {
97172
'intersectionRatio' in IntersectionObserverEntry.prototype
98173
)
99174
}
100-
```
101-
102-
## Props
103-
104-
The **`<Observer />`** accepts the following props:
105-
106-
| Name | Type | Default | Required | Description |
107-
| --------------- | ----------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108-
| **children** | func/node | | true | Children should be either a function or a node |
109-
| **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. |
110-
| **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. |
111-
| **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). |
112-
| **tag** | String | 'div' | false | Element tag to use for the wrapping component |
113-
| **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. |
114-
| **triggerOnce** | Bool | false | false | Only trigger this method once |
115-
| **onChange** | Func | | false | Call this function whenever the in view state changes |
116-
| **render** | Func | | false | Render prop boolean indicating inView state |
117-
| **innerRef** | Func | | false | Get a reference to the the inner DOM node |
118-
119-
## Example code
120-
121-
### Child as function
122-
123-
The default way to use the `Observer`, is to pass a function as the child. It
124-
will be called whenever the state changes, with the new value of `inView`.
125-
126-
```js
127-
import Observer from 'react-intersection-observer'
128-
129-
const Component = () => (
130-
<Observer>
131-
{inView => <h2>{`Header inside viewport ${inView}.`}</h2>}
132-
</Observer>
133-
)
134-
135-
export default Component
136-
```
137-
138-
### Render prop
139-
140-
```js
141-
import Observer from 'react-intersection-observer'
142-
143-
const Component = () => (
144-
<Observer render={inView => <h2>{`Header inside viewport ${inView}.`}</h2>} />
145-
)
146-
147-
export default Component
148-
```
149-
150-
### OnChange callback
151-
152-
You can monitor the onChange method, and control the state in your own
153-
component. The child node will always be rendered.
154-
155-
```js
156-
import Observer from 'react-intersection-observer'
157-
158-
const Component = () => (
159-
<Observer onChange={inView => console.log('Inview:', inView)}>
160-
<h2>Plain children are always rendered. Use onChange to monitor state.</h2>
161-
</Observer>
162-
)
163-
164-
export default Component
165-
```
175+
```

index.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1+
import { RenderProps } from 'react-intersection-observer'
2+
13
declare module 'react-intersection-observer' {
24
import React = require('react')
35

6+
export interface RenderProps {
7+
inView: boolean
8+
ref: Function
9+
}
10+
411
export interface IntersectionObserverProps {
512
/** Children should be either a function or a node */
613
children?: React.ReactNode | ((inView: boolean) => React.ReactNode)
714

815
/** Render prop boolean indicating inView state */
9-
render?: (inView: boolean) => React.ReactNode
16+
render?: (fields: RenderProps) => React.ReactNode
1017

1118
/**
1219
* The `HTMLElement` that is used as the viewport for checking visibility of
@@ -51,13 +58,12 @@ declare module 'react-intersection-observer' {
5158

5259
/** Call this function whenever the in view state changes */
5360
onChange?: (inView: boolean) => void
54-
55-
/** Get a reference to the the inner DOM node */
56-
innerRef?: (element?: HTMLElement) => void
5761
}
5862

5963
export default class IntersectionObserver extends React.Component<
6064
IntersectionObserverProps,
6165
{}
6266
> {}
6367
}
68+
69+
export default Component

package.json

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,38 +57,40 @@
5757
"<rootDir>/jest-setup.js"
5858
]
5959
},
60-
"dependencies": {},
60+
"dependencies": {
61+
"invariant": "^2.2.4"
62+
},
6163
"peerDependencies": {
6264
"react": "^15.0.0 || ^16.0.0 || ^17.0.0"
6365
},
6466
"devDependencies": {
65-
"@storybook/addon-actions": "^3.3.12",
66-
"@storybook/addon-options": "^3.3.12",
67-
"@storybook/react": "^3.3.12",
67+
"@storybook/addon-actions": "^3.4.3",
68+
"@storybook/addon-options": "^3.4.3",
69+
"@storybook/react": "^3.4.3",
6870
"babel-cli": "^6.24.1",
69-
"babel-core": "^6.25.0",
70-
"babel-jest": "^22.1.0",
71+
"babel-core": "^6.26.3",
72+
"babel-jest": "^22.4.3",
7173
"babel-preset-env": "^1.6.1",
7274
"babel-preset-react": "^6.24.1",
7375
"babel-preset-stage-2": "^6.24.1",
7476
"babel-runtime": "^6.25.0",
7577
"concurrently": "3.5.1",
7678
"enzyme": "^3.3.0",
7779
"enzyme-adapter-react-16": "^1.0.4",
78-
"enzyme-to-json": "^3.3.1",
79-
"eslint": "^4.17.0",
80-
"eslint-config-insilico": "^5.1.0",
80+
"enzyme-to-json": "^3.3.3",
81+
"eslint": "^4.19.1",
82+
"eslint-config-insilico": "^5.2.0",
8183
"flow-bin": "^0.71.0",
82-
"flow-copy-source": "^1.2.2",
84+
"flow-copy-source": "^1.3.0",
8385
"husky": "^0.14.3",
8486
"intersection-observer": "^0.5.0",
85-
"jest": "^22.1.4",
87+
"jest": "^22.4.3",
8688
"lint-staged": "^7.0.0",
87-
"prettier": "^1.10.2",
88-
"react": "^16.2.0",
89-
"react-dom": "^16.2.0",
90-
"react-test-renderer": "^16.2.0",
91-
"request": "~2.84.0"
89+
"prettier": "^1.12.1",
90+
"react": "^16.3.2",
91+
"react-dom": "^16.3.2",
92+
"react-test-renderer": "^16.3.2",
93+
"request": "~2.85.0"
9294
},
9395
"resolutions": {
9496
"react": "^16.2.0",

src/index.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @flow
22
import * as React from 'react'
33
import { observe, unobserve } from './intersection'
4+
import invariant from 'invariant'
45

56
type Props = {
67
/** Element tag to use for the wrapping */
@@ -10,7 +11,10 @@ type Props = {
1011
/** Children should be either a function or a node */
1112
children?: ((inView: boolean) => React.Node) | React.Node,
1213
/** Render prop boolean indicating inView state */
13-
render?: (inView: boolean) => React.Node,
14+
render?: ({
15+
inView: boolean,
16+
ref: (node: ?HTMLElement) => void,
17+
}) => React.Node,
1418
/** 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. */
1519
threshold?: number | Array<number>,
1620
/** 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.*/
@@ -22,8 +26,6 @@ type Props = {
2226
rootId?: string,
2327
/** Call this function whenever the in view state changes */
2428
onChange?: (inView: boolean) => void,
25-
/** Get a reference to the the inner DOM node */
26-
innerRef?: Function,
2729
}
2830

2931
type State = {
@@ -50,6 +52,15 @@ class Observer extends React.Component<Props, State> {
5052
inView: false,
5153
}
5254

55+
componentDidMount() {
56+
if (typeof this.props.render === 'function') {
57+
invariant(
58+
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.`,
60+
)
61+
}
62+
}
63+
5364
componentDidUpdate(prevProps: Props, prevState: State) {
5465
// If a IntersectionObserver option changed, reinit the observer
5566
if (
@@ -97,10 +108,6 @@ class Observer extends React.Component<Props, State> {
97108
if (this.node) unobserve(this.node)
98109
this.node = node
99110
this.observeNode()
100-
101-
if (this.props.innerRef) {
102-
this.props.innerRef(node)
103-
}
104111
}
105112

106113
handleChange = (inView: boolean) => {
@@ -115,7 +122,6 @@ class Observer extends React.Component<Props, State> {
115122
children,
116123
render,
117124
tag,
118-
innerRef,
119125
triggerOnce,
120126
threshold,
121127
root,
@@ -126,14 +132,16 @@ class Observer extends React.Component<Props, State> {
126132

127133
const { inView } = this.state
128134

135+
if (typeof render === 'function') {
136+
return render({ inView, ref: this.handleNode })
137+
}
138+
129139
return React.createElement(
130140
tag,
131141
{
132142
...props,
133143
ref: this.handleNode,
134144
},
135-
// If render is a function, use it to render content when in view
136-
typeof render === 'function' ? render(inView) : null,
137145
// If children is a function, render it with the current inView status.
138146
// Otherwise always render children. Assume onChange is being used outside, to control the the state of children.
139147
typeof children === 'function' ? children(inView) : children,

src/intersection.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ function onChange(changes) {
146146
inView = inView && isIntersecting
147147
}
148148

149+
instance.visible = inView
149150
if (instance.callback) {
150151
instance.callback(inView)
151152
}

stories/Observer.story.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import RootComponent from './Root'
99
type Props = {
1010
style?: Object,
1111
children?: React.Node,
12+
innerRef?: Function,
1213
}
1314

1415
const Header = (props: Props) => (
1516
<div
17+
ref={props.innerRef}
1618
style={{
1719
display: 'flex',
1820
minHeight: '25vh',
@@ -41,7 +43,7 @@ storiesOf('Intersection Observer', module)
4143
<ScrollWrapper>
4244
<Observer
4345
onChange={action('Render Observer inview')}
44-
render={inView => (
46+
render={({ inView, ref }) => (
4547
<Header>Header is inside viewport: {inView.toString()}</Header>
4648
)}
4749
/>

0 commit comments

Comments
 (0)