From 56c2f8b9f4d281cdcc8f755ee3cd2e436c96babe Mon Sep 17 00:00:00 2001
From: Daniel Schmidt <3764345+thebuilder@users.noreply.github.com>
Date: Sun, 10 Feb 2019 17:06:09 +0100
Subject: [PATCH] feat(hooks): changes hooks to handle the ref internally
(#170)
* feat(hooks): changes hooks to handle the ref internally
BREAKING CHANGE: YES
---
README.md | 56 +++++-------------
package.json | 5 +-
src/__tests__/hooks.test.js | 21 ++++++-
src/hooks.tsx | 100 ++++++++++++++------------------
src/index.tsx | 2 +-
stories/Hooks.story.tsx | 12 ++--
stories/Observer.story.tsx | 2 +-
stories/ScrollWrapper/index.tsx | 3 +-
yarn.lock | 16 ++---
9 files changed, 95 insertions(+), 122 deletions(-)
diff --git a/README.md b/README.md
index d1f1922f..cbb5ffc6 100644
--- a/README.md
+++ b/README.md
@@ -42,18 +42,24 @@ npm install react-intersection-observer --save
#### `useInView`
+```js
+const [ref, inView, entry] = useInView(options)
+```
+
The new React Hooks, makes it easier then ever to monitor the `inView` state of
-your components. You can import the `useInView` hook, and pass it a `ref` to the
-DOM node you want to observe, alongside some optional [options](#options). It
-will then return `true` once the element enter the viewport.
+your components. Call the `useInView` hook, with the (optional)
+[options](#options) you need. It will return an array containing a `ref`, the
+`inView` status and the current
+[`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
+Assign the `ref` to the DOM element you want to monitor, and the hook will
+report the status.
```jsx
import React, { useRef } from 'react'
import { useInView } from 'react-intersection-observer'
const Component = () => {
- const ref = useRef()
- const inView = useInView(ref, {
+ const [ref, inView] = useInView({
/* Optional options */
threshold: 0,
})
@@ -66,36 +72,6 @@ const Component = () => {
}
```
-#### `useIntersectionObserver`
-
-If you need to know more details about the intersection, you can use the
-`useIntersectionObserver` hook. It takes the same input as `useInView`, but will
-return an object with `inView` and `intersection`. If `intersection` is defined,
-it contains the
-[IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry),
-that triggered the observer.
-
-```jsx
-import React, { useRef } from 'react'
-import { useIntersectionObserver } from 'react-intersection-observer'
-
-const Component = () => {
- const ref = useRef()
- const { inView, intersection } = useIntersectionObserver(ref, {
- threshold: 0,
- })
-
- return (
-
-
{`Header inside viewport ${inView}.`}
-
- {JSON.stringify(intersection || {})}
-
-
- )
-}
-```
-
### Render props
To use the `` component , you pass it a function. It will be called
@@ -161,11 +137,11 @@ argument for the hooks.
The **``** component also accepts the following props:
-| Name | Type | Default | Required | Description |
-| ------------ | ------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| **as** | `string` | | false | Render the wrapping element as this element. Defaults to `div`. |
-| **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 `` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `intersection, giving you more details. |
-| **onChange** | `(inView, entry) => void` | | false | Call this function whenever the in view state changes |
+| Name | Type | Default | Required | Description |
+| ------------ | ------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **as** | `string` | | false | Render the wrapping element as this element. Defaults to `div`. |
+| **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 `` deal with the wrapping element. You will also get the `IntersectionObserverEntry` as `entry, giving you more details. |
+| **onChange** | `(inView, entry) => void` | | false | Call this function whenever the in view state changes |
## Usage in other projects
diff --git a/package.json b/package.json
index c3f5e83c..6a9795fd 100644
--- a/package.json
+++ b/package.json
@@ -132,7 +132,7 @@
"husky": "^1.3.1",
"intersection-observer": "^0.5.1",
"jest": "^24.0.0",
- "jest-dom": "^3.0.2",
+ "jest-dom": "^3.1.0",
"lint-staged": "^8.1.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.16.2",
@@ -150,7 +150,6 @@
"typescript-eslint-parser": "^22.0.0"
},
"resolutions": {
- "ajv": "6.6.1",
"@types/react": "16.8.2"
}
-}
\ No newline at end of file
+}
diff --git a/src/__tests__/hooks.test.js b/src/__tests__/hooks.test.js
index f5e736cf..d692f71b 100644
--- a/src/__tests__/hooks.test.js
+++ b/src/__tests__/hooks.test.js
@@ -1,4 +1,4 @@
-import React, { useRef } from 'react'
+import React from 'react'
import { act } from 'react-dom/test-utils'
import { useInView } from '../hooks'
import { observe, unobserve } from '../intersection'
@@ -11,8 +11,18 @@ afterEach(() => {
})
const HookComponent = ({ options }) => {
- const ref = useRef()
- const inView = useInView(ref, options)
+ const [ref, inView] = useInView(options)
+ return {inView.toString()}
+}
+
+const LazyHookComponent = ({ options }) => {
+ const [isLoading, setIsLoading] = React.useState(true)
+
+ React.useEffect(() => {
+ setIsLoading(false)
+ }, [])
+ const [ref, inView] = useInView(options)
+ if (isLoading) return Loading
return {inView.toString()}
}
@@ -21,6 +31,11 @@ test('should create a hook', () => {
expect(observe).toHaveBeenCalled()
})
+test('should create a lazy hook', () => {
+ render()
+ expect(observe).toHaveBeenCalled()
+})
+
test('should create a hook inView', () => {
observe.mockImplementation((el, callback, options) => {
if (callback) callback(true, {})
diff --git a/src/hooks.tsx b/src/hooks.tsx
index 261a0acc..362ecc20 100644
--- a/src/hooks.tsx
+++ b/src/hooks.tsx
@@ -2,72 +2,56 @@ import * as React from 'react'
import { IntersectionOptions } from './'
import { observe, unobserve } from './intersection'
-export type HookResponse = {
+export type HookResponse = [
+ ((node?: Element | null) => void),
+ boolean,
+ IntersectionObserverEntry | undefined
+]
+
+type State = {
inView: boolean
entry?: IntersectionObserverEntry
}
-export function useIntersectionObserver(
- ref: React.RefObject,
- options: IntersectionOptions = {},
-): HookResponse {
- const [currentRef, setCurrentRef] = React.useState(
- ref.current,
- )
- const [state, setState] = React.useState({
+export function useInView(options: IntersectionOptions = {}): HookResponse {
+ const [ref, setRef] = React.useState(null)
+ const [state, setState] = React.useState({
inView: false,
entry: undefined,
})
- // Create a separate effect that always checks if the ref has changed.
- // If it changes, the Observer will need to be recreated, so set a new ref state
- // that the triggers an update of the next effect.
- React.useEffect(() => {
- if (ref.current !== currentRef) {
- setCurrentRef(ref.current)
- }
- })
-
- React.useEffect(() => {
- if (currentRef) {
- observe(
- currentRef,
- (inView, intersection) => {
- setState({ inView, entry: intersection })
-
- if (inView && options.triggerOnce) {
- // If it should only trigger once, unobserve the element after it's inView
- unobserve(currentRef)
- }
- },
- options,
- )
- }
-
- return () => {
- unobserve(currentRef)
- }
- }, [
- // Only create a new Observer instance if the ref or any of the options have been changed.
- currentRef,
- options.threshold,
- options.root,
- options.rootMargin,
- options.triggerOnce,
- ])
-
- return state
-}
+ React.useEffect(
+ () => {
+ if (ref) {
+ observe(
+ ref,
+ (inView, intersection) => {
+ setState({ inView, entry: intersection })
+
+ if (inView && options.triggerOnce) {
+ // If it should only trigger once, unobserve the element after it's inView
+ unobserve(ref)
+ }
+ },
+ options,
+ )
+ }
+
+ return () => {
+ if (ref) unobserve(ref)
+ }
+ },
+ [
+ // Only create a new Observer instance if the ref or any of the options have been changed.
+ ref,
+ options.threshold,
+ options.root,
+ options.rootMargin,
+ options.triggerOnce,
+ ],
+ )
-/**
- * Hook to observe an Element, and return boolean indicating if it's inside the viewport
- **/
-export function useInView(
- ref: React.RefObject,
- options: IntersectionOptions = {},
-): boolean {
- const intersection = useIntersectionObserver(ref, options)
- React.useDebugValue(intersection.inView)
+ React.useDebugValue(state.inView)
- return intersection.inView
+ return [setRef, state.inView, state.entry]
}
diff --git a/src/index.tsx b/src/index.tsx
index a0fcaf74..90fb2154 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import { observe, unobserve } from './intersection'
import invariant from 'invariant'
-export { useInView, useIntersectionObserver } from './hooks'
+export { useInView } from './hooks'
type RenderProps = {
inView: boolean
diff --git a/stories/Hooks.story.tsx b/stories/Hooks.story.tsx
index 3a7dbae5..08567ecb 100644
--- a/stories/Hooks.story.tsx
+++ b/stories/Hooks.story.tsx
@@ -18,15 +18,14 @@ const sharedStyle: CSSProperties = {
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
- background: 'lightcoral',
+ background: '#148bb4',
color: 'azure',
}
const LazyHookComponent = ({ options, style, children, ...rest }: Props) => {
- const ref = React.useRef(null)
- const inView = useInView(ref, options)
+ const [ref, inView, entry] = useInView(options)
const [isLoading, setIsLoading] = React.useState(true)
- action('Inview')(inView)
+ action('Inview')(inView, entry)
React.useEffect(() => {
setIsLoading(false)
@@ -45,9 +44,8 @@ const LazyHookComponent = ({ options, style, children, ...rest }: Props) => {
)
}
const HookComponent = ({ options, style, children, ...rest }: Props) => {
- const ref = React.useRef(null)
- const inView = useInView(ref, options)
- action('Inview')(inView)
+ const [ref, inView, entry] = useInView(options)
+ action('Inview')(inView, entry)
return (
diff --git a/stories/Observer.story.tsx b/stories/Observer.story.tsx
index 3dbbe79b..7a778ecf 100644
--- a/stories/Observer.story.tsx
+++ b/stories/Observer.story.tsx
@@ -22,7 +22,7 @@ const Header = React.forwardRef
((props: Props, ref) => (
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
- background: 'lightcoral',
+ background: '#148bb4',
color: 'azure',
...props.style,
}}
diff --git a/stories/ScrollWrapper/index.tsx b/stories/ScrollWrapper/index.tsx
index 9cb7977a..82d9e0b2 100644
--- a/stories/ScrollWrapper/index.tsx
+++ b/stories/ScrollWrapper/index.tsx
@@ -8,7 +8,8 @@ const style: CSSProperties = {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
- backgroundColor: 'papayawhip',
+ backgroundColor: '#2d1176',
+ color: '#fff',
}
type Props = {
diff --git a/yarn.lock b/yarn.lock
index 50499a5e..b72625cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1782,10 +1782,10 @@ ajv-keywords@^3.1.0:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.3.0.tgz#cb6499da9b83177af8bc1732b2f0a1a1a3aacf8c"
integrity sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==
-ajv@6.6.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1:
- version "6.6.1"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61"
- integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==
+ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1:
+ version "6.9.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1"
+ integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
@@ -5859,10 +5859,10 @@ jest-docblock@^24.0.0:
dependencies:
detect-newline "^2.1.0"
-jest-dom@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.0.2.tgz#63094a95c721a5647dcaa7a87991c7a9ebf30608"
- integrity sha512-jDnI83LWZgIrlJe7d21SBx2vzcUiuSTErjIMn8bq+Wm/LF/k6XS+6zmpMUaGlykqC05uyWHNeg4K+Qr3atuVEw==
+jest-dom@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.1.0.tgz#a7b57d5152957def86a855614e56b6585becd97b"
+ integrity sha512-TGbg5gHF6TfIOlsoqK57EvHtiGCKAi87xWqqiNk+1S0+hteV6ThCjh/2BrKkMBODKDKR52yfUKM0lrVldi3Z2w==
dependencies:
chalk "^2.4.1"
css "^2.2.3"