Build a Custom TikTok Autoplay React Hook With Intersection Observer

Build a Custom TikTok Autoplay React Hook With Intersection Observer

Have you ever wondered how social media applications like TikTok, Instagram, or Twitter detect a particular video post that is in the viewport, autoplay it, and then stop it immediately after it goes out of view?

In this article, I will explain how Intersection Observer can be used to implement this autoplay and pause feature by creating a React custom Hook for use in a TikTok clone.

As a developer, you might want to implement an autoplay feature in a video player application, lazy load an image, or detect when an advertisement is in the viewport of a user’s browser. With Intersection Observer you can do all these.

What is Intersection Observer?

Intersection Observer is a JavaScript browser API that asynchronously monitors the position of DOM elements with respect to the client’s viewport or a root element.

How Intersection Observer works

Basically, the Intersection Observer API triggers a callback function in specific situations.

These situations include when the position of the selected element comes into the client’s viewport, when a selected element intersects a parent or root element, or when the observer is initially declared.

Specifications and browser compatibility

At the time of writing this article, the specifications are still a working draft. However, updates can be found here.

As for the browser compatibility, here is the current report:

Applications for Intersection Observer

Intersection Observer can be used for a wide variety of various applications outside of the scope of this article.

They include optional rendering of DOM elements, lazy loading, loading content on-demand with infinite scrolling, rendering advertisements and animations, and creating carousels.

The example I am using in this article (creating a custom autoplay Hook for a TikTok clone) can help you become familiar with the Intersection Observer API in order to start exploring the other options it can offer your apps.

Using Intersection Observer

Firstly, we want to find out if our browser supports the Intersection Observer API.

We can write a condition to check, like so:

if ('IntersectionObserver' in window) {
    console.log("IntersectionObserver is supported!");
} else {
    console.log("IntersectionObserver is not supported!");
}

The ItersectionObserver object is usually structured like this:

let options= {
    root: null,
    rootMargin: '0px',
    threshold: 0.5
};

const callback = (entries){ // entries = array of targeted elements
    entries.forEach(entry=>{
        // what happens each entry
    })
}

let observerObj = new IntersectionObserver(callback, options);
observerObj.observe();

Here, the IntersectionObserver object accepts two arguments. The callback function, which is triggered after Intersection Observer is executed, and an optional options object. This is an object with certain properties that determine when, and how, the Intersection Observer works.

The callback function

When the callback the function is executed, a list of targeted elements is checked by the Intersection Observer. These elements all have specific properties.

Examples of those properties are:

  • boundingClientRect:

  • intersectionRatio:

  • intersectionRect

  • isIntersecting

  • rootBounds

  • target

  • time

These properties are used to check the current element’s behavior relative to their root element.

In this article, we will be using isIntersecting to check if the current entry is intersecting with the root. This will be the indicator that our video is in the viewport, and therefore ready to begin playing.

The options object

The options object contains the following properties:

The root is the browser’s viewport by default, or if set as null. If an element is specified as the root, it has to be a parent to the targeted element. The root is what the targeted element needs to intersect with before the callback function is triggered

The rootMargin sets the margin around the root element before detecting the intersection. By default, it is 0 (which triggers the action exactly when the root property enters the viewport), but it can be valued in the same manner as a CSS margin in case you’d like the callback function to occur at a different moment.

The threshold represents what percentage of the targeted element should intersect root before the callback function is executed. It can be either a number or an array of numbers; the accepted values range from 0 to 1.

If it is 0, it means the first pixel of the target element needs to intersect with the root element before the callback function is executed. if it is 0.5, 50 per cent of the target element needs to intersect with the root, and so on.

Targeting an element to be observed

To target an element with JavaScript, we have to use the querySelector function, which will search the DOM for a given id or class.

In React, we can use the useRef Hook to target an element. Then, we pass targetElement as a parameter in the observe function, like so:

/*
In JavaScript we can use querySelector to select a DOM element like this...
*/
let targetElement = document.querySelector('#item')
observerObj.observe(targetElement)

//In React we can use the useRef hook like this...
let targetRef = useRef(null); //Set a component to be ref of targetRef
let targetElement = targetRef.current
observerObj.observe(targetElement)

In the TikTok clone, we will be using the useRef Hook to target each video component in order to track when it comes into the viewport.

Creating an Intersection Observer custom Hook in React

To create a reusable Intersection Observer Hook, we will create a new function called useElementOnScreen and implement Intersection Observer using options and targetRef we passed in as props:

import { useEffect, useMemo, useState } from 'react'
const useElementOnScreen = (options, targetRef) => {
    const [isVisibile, setIsVisible] = useState()
    const callbackFunction = entries => {
        const [entry] = entries //const entry = entries[0]
        setIsVisible(entry.isIntersecting)
    }
    const optionsMemo = useMemo(() => {
        return options
    }, [options])
    useEffect(() => {
        const observer = new IntersectionObserver(callbackFunction, optionsMemo)
        const currentTarget = targetRef.current
        if (currentTarget) observer.observe(currentTarget)

        return () => {
        if(currentTarget) observer.unobserve(currentTarget)
        }
    }, [targetRef, optionsMemo])
    return isVisibile
}
export default useElementOnScreen

Having understood what the options and targetRef are, we need to pass them as props to the useElementOnScreen Hook, as we will need them as parameters for a new Intersection Observer function.

Then, we set a default state for the element’s visibility as null.

Inside the callback function, we are setting the isVisible state to the value returned if the targeted Element isIntersecting (we are always expecting true or false).

After observing the target element, we return the isVisible state. The returned value of isVisible is what we will use to decide when a video should play or stop.

If the isVisible state of a video component is true we play the video, else if it is false we stop the video.

Using the Intersection Observer custom Hook in a TikTok clone

Setting up the application

For the sake of brevity, I have created a starter project that contains the entire source code of the TikTok clone where we will implement the Intersection Observer hook we just created above. It is available on my GitHub repository.

To start the application running, open your terminal to a new work folder and run the following commands:

git clone https://github.com/wolz-CODElife/Tiktok-clone.git

cd Tiktok-clone

npm install

In the folder downloaded, the following files and directories should be present:

The files and folders we are working with are inside the src.As shown above, I have already included the Intersection Observer hook that we created in the previous section of this article in the hooks directory. All that is remaining to do is to import the useElementOnScreen hooks in the TikTok application.

Setting up autoplay

Now, let’s update the Video.js component to play and stop a video depending on its visibility status.

Inside the Video.js file, put the following code:

import React, { useEffect, useRef, useState } from "react";
import "./Video.css";
import VideoFooter from "./VideoFooter";
import VideoSidebar from "./VideoSidebar";
import useElementOnScreen from './hooks/useElementOnScreen'
import VideoPlayButton from "./VideoPlayButton";
const Video = ({ url, channel, description, song, likes, messages, shares }) => {
  const [playing, setPlaying] = useState(false);
  const videoRef = useRef(null);
  const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.3
  }
  const isVisibile = useElementOnScreen(options, videoRef)
  const onVideoClick = () => {
    if (playing) {
      videoRef.current.pause();
      setPlaying(!playing);
    } else {
      videoRef.current.play();
      setPlaying(!playing);
    }
  };
  useEffect(() => {
    if (isVisibile) {
      if (!playing) {        
        videoRef.current.play();
        setPlaying(true)
      }
    }
    else {
      if (playing) {        
        videoRef.current.pause();
        setPlaying(false)
      }
    }
  }, [isVisibile])


  return (
    <div className="video">
      <video className="video_player" loop preload="true" ref={videoRef} onClick={onVideoClick} src={url}></video>
      <VideoFooter channel={channel} description={description} song={song} />
      <VideoSidebar likes={likes} messages={messages} shares={shares} />
      {!playing && <VideoPlayButton onVideoClick={onVideoClick} />}
    </div>
  );
};
export default Video;

Here, we imported the custom Hook (useElementOnScreen), then used the value returned (which could be true or false) as the isVisible value.

Note that we set the following options for the Intersection Observer: root is null, which means we are using the window as a parent element. rootMargin is 0px, and threshold is 0.3 which means once 30 per cent of the target element is in the viewport, the callback function is triggered.

Next, we use UseEffect to change the playing state of the video if the isVisible value changes, like so:

if (isVisibile) {
      if (!playing) {        
        videoRef.current.play();
        setPlaying(true)
      }
    }
    else {
      if (playing) {        
        videoRef.current.pause();
        setPlaying(false)
      }
    }

This code means that, if the video is visible, the playing state is set to true. If it is not yet playing, and if the video is not visible, the playing state is set to false.

With this done, we can run the application with the following:

npm start

If everything goes well, we should have something like this:

If you wish to change the videos or even use a live database, edit the video state in App.js.

Currently, we have the following array of objects:

[
    {
      url: 'https://res.cloudinary.com/codelife/video/upload/v1633232723/tiktok-clone/tiktok2_qxafx3.mp4',
      channel: 'DanceCrew',
      description: 'Video by Lara Jameson from Pexels',
      song: 'Bounce - Ruger',
      likes: 250,
      messages: 120,
      shares: 40
    },
    {
      url: 'https://res.cloudinary.com/codelife/video/upload/v1633232725/tiktok-clone/tiktok1_np37xq.mp4',
      channel: 'Happyfeet',
      description: '#happyfeetlegwork videos on TikTok',
      song: 'Kolo sound - Nathan',
      likes: 250,
      messages: 120,
      shares: 40
    },
    {
      url: 'https://res.cloudinary.com/codelife/video/upload/v1633232726/tiktok-clone/tiktok3_scmwvk.mp4',
      channel: 'thiskpee',
      description: 'The real big thug boys💛🦋 The real big thug boys💛🦋 ',
      song: 'original sound - KALEI KING 🦋',
      likes: 250,
      messages: 120,
      shares: 40
    },
  ]

Conclusion

Having created the application successfully, we should have learned how Intersection Observer works and how you can use it to implement an autoplay feature similar to that in TikTok or Instagram.

With this knowledge, you can try implementing lazy loading images, carousels or even an infinitely scrolling blog feeds page!

You can check the live demo of my TikTok clone here.I advise viewing it on a desktop browser for the best experience.

If you have any questions or remarks, please feel free to let me know in the comments.