UNPKG

react-intersection-observer-ng

Version:

Monitor if a component is inside the viewport, using IntersectionObserver API

298 lines (234 loc) 12.7 kB
# 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