Skip to content

Commit 56c2f8b

Browse files
authored
feat(hooks): changes hooks to handle the ref internally (#170)
* feat(hooks): changes hooks to handle the ref internally BREAKING CHANGE: YES
1 parent 27ec931 commit 56c2f8b

File tree

9 files changed

+95
-122
lines changed

9 files changed

+95
-122
lines changed

README.md

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,24 @@ npm install react-intersection-observer --save
4242

4343
#### `useInView`
4444

45+
```js
46+
const [ref, inView, entry] = useInView(options)
47+
```
48+
4549
The new React Hooks, makes it easier then ever to monitor the `inView` state of
46-
your components. You can import the `useInView` hook, and pass it a `ref` to the
47-
DOM node you want to observe, alongside some optional [options](#options). It
48-
will then return `true` once the element enter the viewport.
50+
your components. Call the `useInView` hook, with the (optional)
51+
[options](#options) you need. It will return an array containing a `ref`, the
52+
`inView` status and the current
53+
[`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
54+
Assign the `ref` to the DOM element you want to monitor, and the hook will
55+
report the status.
4956

5057
```jsx
5158
import React, { useRef } from 'react'
5259
import { useInView } from 'react-intersection-observer'
5360

5461
const Component = () => {
55-
const ref = useRef()
56-
const inView = useInView(ref, {
62+
const [ref, inView] = useInView({
5763
/* Optional options */
5864
threshold: 0,
5965
})
@@ -66,36 +72,6 @@ const Component = () => {
6672
}
6773
```
6874

69-
#### `useIntersectionObserver`
70-
71-
If you need to know more details about the intersection, you can use the
72-
`useIntersectionObserver` hook. It takes the same input as `useInView`, but will
73-
return an object with `inView` and `intersection`. If `intersection` is defined,
74-
it contains the
75-
[IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry),
76-
that triggered the observer.
77-
78-
```jsx
79-
import React, { useRef } from 'react'
80-
import { useIntersectionObserver } from 'react-intersection-observer'
81-
82-
const Component = () => {
83-
const ref = useRef()
84-
const { inView, intersection } = useIntersectionObserver(ref, {
85-
threshold: 0,
86-
})
87-
88-
return (
89-
<div ref={ref}>
90-
<h2>{`Header inside viewport ${inView}.`}</h2>
91-
<pre>
92-
<code>{JSON.stringify(intersection || {})}</code>
93-
</pre>
94-
</div>
95-
)
96-
}
97-
```
98-
9975
### Render props
10076

10177
To use the `<InView>` component , you pass it a function. It will be called
@@ -161,11 +137,11 @@ argument for the hooks.
161137

162138
The **`<InView />`** component also accepts the following props:
163139

164-
| Name | Type | Default | Required | Description |
165-
| ------------ | ------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
166-
| **as** | `string` | | false | Render the wrapping element as this element. Defaults to `div`. |
167-
| **children** | `Function`, `ReactNode` | | true | Children expects a function that receives an object contain an `inView` boolean and `ref` that should be assigned to the element root. Alternately pass a plain child, to have the `<Observer />` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `intersection, giving you more details. |
168-
| **onChange** | `(inView, entry) => void` | | false | Call this function whenever the in view state changes |
140+
| Name | Type | Default | Required | Description |
141+
| ------------ | ------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142+
| **as** | `string` | | false | Render the wrapping element as this element. Defaults to `div`. |
143+
| **children** | `Function`, `ReactNode` | | true | Children expects a function that receives an object contain an `inView` boolean and `ref` that should be assigned to the element root. Alternately pass a plain child, to have the `<Observer />` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `entry, giving you more details. |
144+
| **onChange** | `(inView, entry) => void` | | false | Call this function whenever the in view state changes |
169145

170146
## Usage in other projects
171147

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
"husky": "^1.3.1",
133133
"intersection-observer": "^0.5.1",
134134
"jest": "^24.0.0",
135-
"jest-dom": "^3.0.2",
135+
"jest-dom": "^3.1.0",
136136
"lint-staged": "^8.1.1",
137137
"npm-run-all": "^4.1.5",
138138
"prettier": "^1.16.2",
@@ -150,7 +150,6 @@
150150
"typescript-eslint-parser": "^22.0.0"
151151
},
152152
"resolutions": {
153-
"ajv": "6.6.1",
154153
"@types/react": "16.8.2"
155154
}
156-
}
155+
}

src/__tests__/hooks.test.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from 'react'
1+
import React from 'react'
22
import { act } from 'react-dom/test-utils'
33
import { useInView } from '../hooks'
44
import { observe, unobserve } from '../intersection'
@@ -11,8 +11,18 @@ afterEach(() => {
1111
})
1212

1313
const HookComponent = ({ options }) => {
14-
const ref = useRef()
15-
const inView = useInView(ref, options)
14+
const [ref, inView] = useInView(options)
15+
return <div ref={ref}>{inView.toString()}</div>
16+
}
17+
18+
const LazyHookComponent = ({ options }) => {
19+
const [isLoading, setIsLoading] = React.useState(true)
20+
21+
React.useEffect(() => {
22+
setIsLoading(false)
23+
}, [])
24+
const [ref, inView] = useInView(options)
25+
if (isLoading) return <div>Loading</div>
1626
return <div ref={ref}>{inView.toString()}</div>
1727
}
1828

@@ -21,6 +31,11 @@ test('should create a hook', () => {
2131
expect(observe).toHaveBeenCalled()
2232
})
2333

34+
test('should create a lazy hook', () => {
35+
render(<LazyHookComponent />)
36+
expect(observe).toHaveBeenCalled()
37+
})
38+
2439
test('should create a hook inView', () => {
2540
observe.mockImplementation((el, callback, options) => {
2641
if (callback) callback(true, {})

src/hooks.tsx

Lines changed: 42 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,56 @@ import * as React from 'react'
22
import { IntersectionOptions } from './'
33
import { observe, unobserve } from './intersection'
44

5-
export type HookResponse = {
5+
export type HookResponse = [
6+
((node?: Element | null) => void),
7+
boolean,
8+
IntersectionObserverEntry | undefined
9+
]
10+
11+
type State = {
612
inView: boolean
713
entry?: IntersectionObserverEntry
814
}
915

10-
export function useIntersectionObserver(
11-
ref: React.RefObject<Element>,
12-
options: IntersectionOptions = {},
13-
): HookResponse {
14-
const [currentRef, setCurrentRef] = React.useState<Element | null>(
15-
ref.current,
16-
)
17-
const [state, setState] = React.useState<HookResponse>({
16+
export function useInView(options: IntersectionOptions = {}): HookResponse {
17+
const [ref, setRef] = React.useState<Element | null | undefined>(null)
18+
const [state, setState] = React.useState<State>({
1819
inView: false,
1920
entry: undefined,
2021
})
2122

22-
// Create a separate effect that always checks if the ref has changed.
23-
// If it changes, the Observer will need to be recreated, so set a new ref state
24-
// that the triggers an update of the next effect.
25-
React.useEffect(() => {
26-
if (ref.current !== currentRef) {
27-
setCurrentRef(ref.current)
28-
}
29-
})
30-
31-
React.useEffect(() => {
32-
if (currentRef) {
33-
observe(
34-
currentRef,
35-
(inView, intersection) => {
36-
setState({ inView, entry: intersection })
37-
38-
if (inView && options.triggerOnce) {
39-
// If it should only trigger once, unobserve the element after it's inView
40-
unobserve(currentRef)
41-
}
42-
},
43-
options,
44-
)
45-
}
46-
47-
return () => {
48-
unobserve(currentRef)
49-
}
50-
}, [
51-
// Only create a new Observer instance if the ref or any of the options have been changed.
52-
currentRef,
53-
options.threshold,
54-
options.root,
55-
options.rootMargin,
56-
options.triggerOnce,
57-
])
58-
59-
return state
60-
}
23+
React.useEffect(
24+
() => {
25+
if (ref) {
26+
observe(
27+
ref,
28+
(inView, intersection) => {
29+
setState({ inView, entry: intersection })
30+
31+
if (inView && options.triggerOnce) {
32+
// If it should only trigger once, unobserve the element after it's inView
33+
unobserve(ref)
34+
}
35+
},
36+
options,
37+
)
38+
}
39+
40+
return () => {
41+
if (ref) unobserve(ref)
42+
}
43+
},
44+
[
45+
// Only create a new Observer instance if the ref or any of the options have been changed.
46+
ref,
47+
options.threshold,
48+
options.root,
49+
options.rootMargin,
50+
options.triggerOnce,
51+
],
52+
)
6153

62-
/**
63-
* Hook to observe an Element, and return boolean indicating if it's inside the viewport
64-
**/
65-
export function useInView(
66-
ref: React.RefObject<Element>,
67-
options: IntersectionOptions = {},
68-
): boolean {
69-
const intersection = useIntersectionObserver(ref, options)
70-
React.useDebugValue(intersection.inView)
54+
React.useDebugValue(state.inView)
7155

72-
return intersection.inView
56+
return [setRef, state.inView, state.entry]
7357
}

src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
22
import { observe, unobserve } from './intersection'
33
import invariant from 'invariant'
4-
export { useInView, useIntersectionObserver } from './hooks'
4+
export { useInView } from './hooks'
55

66
type RenderProps = {
77
inView: boolean

stories/Hooks.story.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,14 @@ const sharedStyle: CSSProperties = {
1818
justifyContent: 'center',
1919
alignItems: 'center',
2020
textAlign: 'center',
21-
background: 'lightcoral',
21+
background: '#148bb4',
2222
color: 'azure',
2323
}
2424

2525
const LazyHookComponent = ({ options, style, children, ...rest }: Props) => {
26-
const ref = React.useRef<HTMLDivElement>(null)
27-
const inView = useInView(ref, options)
26+
const [ref, inView, entry] = useInView(options)
2827
const [isLoading, setIsLoading] = React.useState(true)
29-
action('Inview')(inView)
28+
action('Inview')(inView, entry)
3029

3130
React.useEffect(() => {
3231
setIsLoading(false)
@@ -45,9 +44,8 @@ const LazyHookComponent = ({ options, style, children, ...rest }: Props) => {
4544
)
4645
}
4746
const HookComponent = ({ options, style, children, ...rest }: Props) => {
48-
const ref = React.useRef<HTMLDivElement>(null)
49-
const inView = useInView(ref, options)
50-
action('Inview')(inView)
47+
const [ref, inView, entry] = useInView(options)
48+
action('Inview')(inView, entry)
5149

5250
return (
5351
<div ref={ref} style={{ ...sharedStyle, ...style }} {...rest}>

stories/Observer.story.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Header = React.forwardRef<any, Props>((props: Props, ref) => (
2222
justifyContent: 'center',
2323
alignItems: 'center',
2424
textAlign: 'center',
25-
background: 'lightcoral',
25+
background: '#148bb4',
2626
color: 'azure',
2727
...props.style,
2828
}}

stories/ScrollWrapper/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const style: CSSProperties = {
88
flexDirection: 'column',
99
alignItems: 'center',
1010
justifyContent: 'center',
11-
backgroundColor: 'papayawhip',
11+
backgroundColor: '#2d1176',
12+
color: '#fff',
1213
}
1314

1415
type Props = {

yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,10 +1782,10 @@ ajv-keywords@^3.1.0:
17821782
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.3.0.tgz#cb6499da9b83177af8bc1732b2f0a1a1a3aacf8c"
17831783
integrity sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==
17841784

1785-
ajv@6.6.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1:
1786-
version "6.6.1"
1787-
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61"
1788-
integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==
1785+
ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1:
1786+
version "6.9.1"
1787+
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1"
1788+
integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==
17891789
dependencies:
17901790
fast-deep-equal "^2.0.1"
17911791
fast-json-stable-stringify "^2.0.0"
@@ -5859,10 +5859,10 @@ jest-docblock@^24.0.0:
58595859
dependencies:
58605860
detect-newline "^2.1.0"
58615861

5862-
jest-dom@^3.0.2:
5863-
version "3.0.2"
5864-
resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.0.2.tgz#63094a95c721a5647dcaa7a87991c7a9ebf30608"
5865-
integrity sha512-jDnI83LWZgIrlJe7d21SBx2vzcUiuSTErjIMn8bq+Wm/LF/k6XS+6zmpMUaGlykqC05uyWHNeg4K+Qr3atuVEw==
5862+
jest-dom@^3.1.0:
5863+
version "3.1.0"
5864+
resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.1.0.tgz#a7b57d5152957def86a855614e56b6585becd97b"
5865+
integrity sha512-TGbg5gHF6TfIOlsoqK57EvHtiGCKAi87xWqqiNk+1S0+hteV6ThCjh/2BrKkMBODKDKR52yfUKM0lrVldi3Z2w==
58665866
dependencies:
58675867
chalk "^2.4.1"
58685868
css "^2.2.3"

0 commit comments

Comments
 (0)