react-intersection-observer-ng
Version:
Monitor if a component is inside the viewport, using IntersectionObserver API
298 lines (234 loc) • 12.7 kB
Markdown
# react-intersection-observer
[![Version Badge][npm-version-svg]][package-url]
[![GZipped size][npm-minzip-svg]][bundlephobia-url]
[![Build Status][travis-svg]][travis-url]
[![Coverage Statu][coveralls-svg]][coveralls-url]
[![dependency status][deps-svg]][deps-url]
[![dev dependency status][dev-deps-svg]][dev-deps-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]
[![Greenkeeper badge][greenkeeper-svg]][greenkeeper-url]
[![styled with prettier][prettier-svg]][prettier-url]
React implementation of the
[Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
to tell you when an element enters or leaves the viewport. Contains both a
[Hooks](#hooks), [render props](#render-props) and
[plain children](#plain-children) implementation.
**Storybook Demo:** https://thebuilder.github.io/react-intersection-observer/
## Features
- 🎣 **Hooks or Component API** - With `useInView` it's easier than ever to
monitor elements
- ⚡️ **Optimized performance** - Auto reuses Intersection Observer instances
where possible
- ⚙️ **Matches native API** - Intuitive to use
- 🌳 **Tree-shakeable** - Only include the parts you use
- 💥 **Tiny bundle** [~1.9 kB gzipped][bundlephobia-url]
## Installation
Install using [Yarn](https://yarnpkg.com):
```sh
yarn add react-intersection-observer
```
or NPM:
```sh
npm install react-intersection-observer --save
```
> ⚠️ You also want to add the
> [intersection-observer](https://www.npmjs.com/package/react-intersection-observer)
> polyfill for full browser support. Check out adding the [polyfill](#polyfill)
> for details about how you can include it.
## Usage
### Hooks 🎣
#### `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. 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, inView] = useInView({
/* Optional options */
threshold: 0,
})
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
)
}
```
### Render props
To use the `<InView>` component , you pass it a function. It will be called
whenever the state changes, with the new value of `inView`. In addition to the
`inView` prop, children also receives a `ref` that should be set on the
containing DOM element. This is the element that the IntersectionObserver will
monitor.
```jsx
import { InView } from 'react-intersection-observer'
const Component = () => (
<InView>
{({ inView, ref }) => (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
)}
</InView>
)
export default Component
```
### Plain children
You can pass any element to the `<InView />`, and it will handle creating the
wrapping DOM element. Add a handler to the `onChange` method, and control the
state in your own component. It will pass any extra props to the HTML element,
allowing you set the `className`, `style`, etc.
```jsx
import { InView } from 'react-intersection-observer'
const Component = () => (
<InView as="div" onChange={inView => console.log('Inview:', inView)}>
<h2>Plain children are always rendered. Use onChange to monitor state.</h2>
</InView>
)
export default Component
```
> ⚠️ When rendering a plain child, make sure you keep your HTML output semantic.
> Change the `as` to match the context, and add a `className` to style the
> `<InView />`.
## API
### Options
Provide these as props on the **`<InView />`** component and as the options
argument for the hooks.
| Name | Type | Default | Required | Description |
| --------------- | ------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **root** | Element | window | false | The Element that is used as the viewport for checking visibility of the target. Defaults to the browser viewport (`window`) if not specified or if null. |
| **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). |
| **threshold** | number \| number[] | 0 | false | Number between 0 and 1 indicating the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. |
| **triggerOnce** | boolean | false | false | Only trigger this method once |
### InView Props
The **`<InView />`** component also accepts the following props:
| Name | Type | Default | Required | Description |
| ------------ | ------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **as** | `string` | 'div' | 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 |
## Testing
In order to write meaningful tests, the `IntersectionObserver` needs to be
mocked. If you are writing your tests in Jest, you can use the included
`test-utils.js`. It mocks the `IntersectionObserver`, and includes a few methods
to assist with faking the `inView` state.
### `test-utils.js`
Import the methods from `react-intersection-observer/test-utils`.
**`mockAllIsIntersecting(isIntersecting:boolean)`**
Set the `isIntersecting` on all current IntersectionObserver instances.
**`mockIsIntersecting(element:Element, isIntersecting:boolean)`**
Set the `isIntersecting` for the IntersectionObserver of a specific element.
**`intersectionMockInstance(element:Element): IntersectionObserver`**
Call the `intersectionMockInstance` method with an element, to get the (mocked)
`IntersectionObserver` instance. You can use this to spy on the `observe` and
`unobserve` methods.
### Test Example
```js
import React from 'react'
import { render } from 'react-testing-library'
import { useInView } from 'react-intersection-observer'
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils'
const HookComponent = ({ options }) => {
const [ref, inView] = useInView(options)
return <div ref={ref}>{inView.toString()}</div>
}
test('should create a hook inView', () => {
const { getByText } = render(<HookComponent />)
// This causes all (existing) IntersectionObservers to be set as intersecting
mockAllIsIntersecting(true)
getByText('true')
})
```
## Built using `react-intersection-observer`
### [Sticks 'n' Sushi](https://sticksnsushi.com/en)
The new brand site for **Sticks 'n' Sushi** is filled with scroll based
animations. All of these are triggered by `react-intersection-observer`, with
[react-scroll-percentage](https://github.com/thebuilder/react-scroll-percentage)
controlling the animations.
## Intersection Observer
[Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
is the API is used to determine if an element is inside the viewport or not.
Browser support is pretty good, but Safari is still missing support.
<a href="http://caniuse.com/#feat=intersectionobserver">
<picture>
<source
type="image/webp"
srcset="https://res.cloudinary.com/ireaderinokun/image/upload/v1549540370/caniuse-embed/intersectionobserver-2019-2-7.webp"
/>
<source
type="image/png"
srcset="https://res.cloudinary.com/ireaderinokun/image/upload/v1549540370/caniuse-embed/intersectionobserver-2019-2-7.png"
/>
<source
type="image/jpeg"
srcset="https://res.cloudinary.com/ireaderinokun/image/upload/v1549540370/caniuse-embed/intersectionobserver-2019-2-7.jpg"
/>
<img
src="https://res.cloudinary.com/ireaderinokun/image/upload/v1549540370/caniuse-embed/intersectionobserver-2019-2-7.png"
alt="Data on support for the intersectionobserver feature across the major browsers from caniuse.com"
/>
</picture>
</a>
### Polyfill
You can import the
[polyfill](https://www.npmjs.com/package/intersection-observer) directly or use
a service like [polyfill.io](https://polyfill.io/v2/docs/) to add it when
needed.
```sh
yarn add intersection-observer
```
Then import it in your app:
```js
import 'intersection-observer'
```
If you are using Webpack (or similar) you could use
[dynamic imports](https://webpack.js.org/api/module-methods/#import-), to load
the Polyfill only if needed. A basic implementation could look something like
this:
```js
/**
* Do feature detection, to figure out which polyfills needs to be imported.
**/
async function loadPolyfills() {
if (typeof window.IntersectionObserver === 'undefined') {
await import('intersection-observer')
}
}
```
[package-url]: https://npmjs.org/package/react-intersection-observer
[npm-version-svg]: https://img.shields.io/npm/v/react-intersection-observer.svg
[npm-minzip-svg]: https://img.shields.io/bundlephobia/minzip/react.svg
[bundlephobia-url]:
https://bundlephobia.com/result?p=react-intersection-observer
[travis-svg]: https://travis-ci.org/thebuilder/react-intersection-observer.svg
[travis-url]: https://travis-ci.org/thebuilder/react-intersection-observer
[coveralls-svg]:
https://coveralls.io/repos/github/thebuilder/react-intersection-observer/badge.svg?branch=master
[coveralls-url]:
https://coveralls.io/github/thebuilder/react-intersection-observer?branch=master
[deps-svg]: https://david-dm.org/thebuilder/react-intersection-observer.svg
[deps-url]: https://david-dm.org/thebuilder/react-intersection-observer
[dev-deps-svg]:
https://david-dm.org/thebuilder/react-intersection-observer/dev-status.svg
[dev-deps-url]:
https://david-dm.org/thebuilder/react-intersection-observer#info=devDependencies
[license-image]: http://img.shields.io/npm/l/react-intersection-observer.svg
[license-url]: LICENSE
[downloads-image]: http://img.shields.io/npm/dm/react-intersection-observer.svg
[downloads-url]:
http://npm-stat.com/charts.html?package=react-intersection-observer
[greenkeeper-svg]:
https://badges.greenkeeper.io/thebuilder/react-intersection-observer.svg
[greenkeeper-url]: https://greenkeeper.io/
[prettier-svg]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg
[prettier-url]: https://github.com/prettier/prettier