JSPM

  • Created
  • Published
  • Downloads 29086
  • Score
    100M100P100Q154256F
  • License MIT

React hook to monitor an element enters or leaves the viewport (or another element).

Package Exports

  • react-cool-inview

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (react-cool-inview) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

🚧 This package is in-progress, API may be changed frequently. I don't recommend you to use it now. If you're willing to be an early user. Please note any changes via release. Here's the milestone.

React Cool Inview

A React hook that monitors an element enters or leaves the viewport (or another element) with performant and efficient way, using Intersection Observer. It's lightweight and super flexible, which can help you do many things, like lazy-loading images and videos, infinite scrolling web app, triggering animations, tracking impressions etc. Try it you will πŸ‘πŸ» it!

build status coverage status npm version npm downloads npm downloads npm bundle size MIT licensed All Contributors PRs welcome Twitter URL

Milestone

  • Detect an element is in-view or not
  • onEnter, onLeave, onChange events
  • Trigger once feature
  • Server-side rendering compatibility
  • Support Intersection Observer v2
  • Unit testing
  • Demo app
  • Demo code
  • Typescript type definition
  • Documentation

Features

Coming soon...

Requirement

To use react-cool-inview, you must use react@16.8.0 or greater which includes hooks.

Installation

This package is distributed via npm.

$ yarn add react-cool-inview
# or
$ npm install --save react-cool-inview

Usage

react-cool-inview has a flexible API design, it can cover simple to complex use cases for you. Here are some ideas for how you can use it.

⚠️ Most modern browsers support Intersection Observer natively. You can also add polyfill for full browser support.

Basic Use Case

To monitor an element enters or leaves the browser viewport by the inView state and useful sugar events.

import React, { useRef } from 'react';
import useInView from 'react-cool-inview';

const App = () => {
  const ref = useRef();
  const { inView, entry } = useInView(ref, {
    threshold: 0.25, // Default is 0
    onEnter: ({ entry, observe, unobserve }) => {
      // Triggered when the target enters the browser viewport (start intersecting)
    },
    onLeave: ({ entry, observe, unobserve }) => {
      // Triggered when the target leaves the browser viewport (end intersecting)
    },
    // More useful options...
  });

  return <div ref={ref}>{inView ? 'Hello, I am πŸ€—' : 'Bye, I am 😴'}</div>;
};

Lazy-loading Images

It's super easy to build an image lazy-loading component with react-cool-inview to boost the performance of your web app.

import React, { useRef } from 'react';
import useInView from 'react-cool-inview';

const LazyImage = ({ width, height, ...rest }) => {
  const ref = useRef();
  const { inView } = useInView(ref, {
    // Stop observe when meet the threshold, so the "inView" only triggered once
    unobserveOnEnter: true,
    // For better UX, we can grow the root margin so the image will be loaded before it comes to the viewport
    rootMargin: '50px',
  });

  return (
    <div className="placeholder" style={{ width, height }} ref={ref}>
      {inView && <img {...rest} />}
    </div>
  );
};

πŸ’‘ Looking for a comprehensive image component? Try react-cool-img, it's my other component library.

Infinite Scrolling

Infinite scrolling is a popular design technique like Facebook and Twitter feed etc., new content being loaded as you scroll down a page. The basic concept as below.

import React, { useRef, useState } from 'react';
import useInView from 'react-cool-inview';
import axios from 'axios';

const App = () => {
  const [todos, setTodos] = useState(['todo-1', 'todo-2', '...']);
  const ref = useRef();

  useInView(ref, {
    // For better UX, we can grow the root margin so the data will be loaded before a user sees the loading indicator
    rootMargin: '50px 0',
    // When the loading indicator comes to the viewport
    onEnter: ({ unobserve, observe }) => {
      // Pause observe when loading data
      unobserve();
      // Load more data
      axios.get('/todos').then((res) => {
        setTodos([...todos, ...res.todos]);
        // Resume observe after loading data
        observe();
      });
    },
  });

  return (
    <div>
      {todos.map((todo) => (
        <div>{todo}</div>
      ))}
      <div ref={ref}>Loading...</div>
    </div>
  );
};

Compare to pagination, infinite scrolling provides a seamless experience for users and it’s easy to see the appeal. But when it comes to render a large lists, performance will be a problem. We can use react-window to address the problem by the technique of DOM recycling.

import React, { useRef, useState } from 'react';
import useInView from 'react-cool-inview';
import { FixedSizeList as List } from 'react-window';
import axios from 'axios';

const Row = ({ index, data, style }) => {
  const { todos, handleLoadingInView } = data;
  const isLast = index === todos.length;
  const ref = useRef();

  useInView(ref, { onEnter: handleLoadingInView });

  return (
    <div style={style} ref={isLast ? ref : null}>
      {isLast ? 'Loading...' : todos[index]}
    </div>
  );
};

const App = () => {
  const [todos, setTodos] = useState(['todo-1', 'todo-2', '...']);
  const [isFetching, setIsFetching] = useState(false);

  const handleLoadingInView = () => {
      // Row component is dynamically created by react-window, we need to use the "isFetching" flag
      // instead of unobserve/observe to avoid re-fetching data
      if (!isFetching)
        axios.get('/todos').then((res) => {
          setTodos([...todos, ...res.todos]);
          setIsFetching(false);
        });

      setIsFetching(true);
    },

  // Leverage the power of react-window to help us address the performance bottleneck
  return (
    <List
      height={150}
      itemCount={todos.length + 1} // Last one is for the loading indicator
      itemSize={35}
      width={300}
      itemData={{ todos, handleLoadingInView }}
    >
      {Row}
    </List>
  );
};

Trigger Animations

Another great use case is to trigger CSS animations once they are visible to the users.

import React, { useRef } from 'react';
import useInView from 'react-cool-inview';

const App = () => {
  const ref = useRef();
  const { inView } = useInView(ref, {
    // Stop observe when meet the threshold, so the "inView" only triggered once
    unobserveOnEnter: true,
    // Shrink the root margin, so the animation will be triggered once an element reach a fixed amount of visible
    rootMargin: '-100px 0',
  });

  return (
    <div className="container" ref={ref}>
      <div className={inView ? 'fade-in' : ''}>I'm a 🍟</div>
    </div>
  );
};

Track Impressions

react-cool-inview can also play as an impression tracker, helps you fire an analytic event when a user sees an element or advertisement.

import React, { useRef } from 'react';
import useInView from 'react-cool-inview';

const App = () => {
  const ref = useRef();
  useInView(ref, {
    // For an element to be considered "seen", we'll say it must be 100% in the viewport
    threshold: 1,
    onEnter: ({ unobserve }) => {
      // Stop observe when meet the threshold, so the "onEnter" only triggered once
      unobserve();
      // Fire an analytic event to your tracking service
      someTrackingService.send('πŸ‹ is seen');
    },
  });

  return <div ref={ref}>I'm a πŸ‹</div>;
};

Intersection Observer v2

The Intersection Observer v1 can perfectly tell you when an element is scrolled into the viewport, but it doesn't tell you whether the element is covered by something else on the page or whether the element has any visual effects applied on it (like transform, opacity, filter etc.) that can make it invisible. The main concern that has surfaced is how this kind of knowledge could be helpful in preventing clickjacking and UI redress attacks (read this article to learn more).

If you want to track the click-through rate (CTR) or impression of an element, which is actually visible to a user, Intersection Observer v2 can be the savior. Which introduces a new boolean field named isVisible. A true value guarantees that an element is visible on the page and has no visual effects applied on it. A false value is just the opposite. When using the v2, there're something we need to know:

  • Check browser compatibility, if a browser doesn't support Intersection Observer v2 the isVisible will be undefined.
  • Understand how visibility is calculated.
  • Visibility is much more expensive to compute than intersection, only use it when needed.

To enable Intersection Observer v2, we need to set the trackVisibility and delay options.

import React, { useRef } from 'react';
import useInView from 'react-cool-inview';

const App = () => {
  const ref = useRef();
  const { inView, isVisible } = useInView(ref, {
    // Track the actual visibility of the element
    trackVisibility: true,
    // Set a minimum delay between notifications, it must be set to 100 (ms) or greater
    // For performance perspective, use the largest tolerable value as much as possible
    delay: 100,
  });
  // If the browser doesn't support Intersection Observer v2, falling back to v1 behavior
  const visible = isVisible || inView;

  return <div ref={ref}>{visible ? 'Hello, I am πŸ€—' : 'Bye, I am 😴'}</div>;
};

API

Coming soon...

Intersection Observer Polyfill

Intersection Observer has good support amongst browsers, but it's not universal. You'll need to polyfill browsers that don't support it. Polyfills is something you should do consciously at the application level. Therefore react-cool-inview doesn't include it.

You can use W3C's polyfill:

$ yarn add intersection-observer
# or
$ npm install --save intersection-observer

Then import it at your app's entry point:

import 'intersection-observer';

Or load the polyfill only if needed:

if (!window.IntersectionObserver) require('intersection-observer');

Polyfill.io is an alternative way to add the polyfill when needed.

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Welly

πŸ’» πŸ“– 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!