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"