Skip to content

Commit

Permalink
feat(hooks): changes hooks to handle the ref internally (#170)
Browse files Browse the repository at this point in the history
* feat(hooks): changes hooks to handle the ref internally

BREAKING CHANGE: YES
  • Loading branch information
thebuilder authored Feb 10, 2019
1 parent 27ec931 commit 56c2f8b
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 122 deletions.
56 changes: 16 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand All @@ -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 (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
<pre>
<code>{JSON.stringify(intersection || {})}</code>
</pre>
</div>
)
}
```

### Render props

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

The **`<InView />`** 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 `<Observer />` 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 `<Observer />` 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

Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -150,7 +150,6 @@
"typescript-eslint-parser": "^22.0.0"
},
"resolutions": {
"ajv": "6.6.1",
"@types/react": "16.8.2"
}
}
}
21 changes: 18 additions & 3 deletions src/__tests__/hooks.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,8 +11,18 @@ afterEach(() => {
})

const HookComponent = ({ options }) => {
const ref = useRef()
const inView = useInView(ref, options)
const [ref, inView] = useInView(options)
return <div ref={ref}>{inView.toString()}</div>
}

const LazyHookComponent = ({ options }) => {
const [isLoading, setIsLoading] = React.useState(true)

React.useEffect(() => {
setIsLoading(false)
}, [])
const [ref, inView] = useInView(options)
if (isLoading) return <div>Loading</div>
return <div ref={ref}>{inView.toString()}</div>
}

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

test('should create a lazy hook', () => {
render(<LazyHookComponent />)
expect(observe).toHaveBeenCalled()
})

test('should create a hook inView', () => {
observe.mockImplementation((el, callback, options) => {
if (callback) callback(true, {})
Expand Down
100 changes: 42 additions & 58 deletions src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element>,
options: IntersectionOptions = {},
): HookResponse {
const [currentRef, setCurrentRef] = React.useState<Element | null>(
ref.current,
)
const [state, setState] = React.useState<HookResponse>({
export function useInView(options: IntersectionOptions = {}): HookResponse {
const [ref, setRef] = React.useState<Element | null | undefined>(null)
const [state, setState] = React.useState<State>({
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<Element>,
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]
}
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 5 additions & 7 deletions stories/Hooks.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(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)
Expand All @@ -45,9 +44,8 @@ const LazyHookComponent = ({ options, style, children, ...rest }: Props) => {
)
}
const HookComponent = ({ options, style, children, ...rest }: Props) => {
const ref = React.useRef<HTMLDivElement>(null)
const inView = useInView(ref, options)
action('Inview')(inView)
const [ref, inView, entry] = useInView(options)
action('Inview')(inView, entry)

return (
<div ref={ref} style={{ ...sharedStyle, ...style }} {...rest}>
Expand Down
2 changes: 1 addition & 1 deletion stories/Observer.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const Header = React.forwardRef<any, Props>((props: Props, ref) => (
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
background: 'lightcoral',
background: '#148bb4',
color: 'azure',
...props.style,
}}
Expand Down
3 changes: 2 additions & 1 deletion stories/ScrollWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const style: CSSProperties = {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'papayawhip',
backgroundColor: '#2d1176',
color: '#fff',
}

type Props = {
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 56c2f8b

Please sign in to comment.