Track Image impressions in Next.js

Emadamerho-Atori Nefe

An impression in web statistics counts how many times a user views a particular element on a website. The element could be an ad, image, or something else.

Impressions are an essential piece of data that analytics software tracks.

In this article, we will learn how to use react-intersection-observer to track image impressions and then how to store them in our browser’s localStorage.

Sandbox

The completed project is on Codesandbox. Fork it and run the code.

1<Codesandbox id="image-intersection-observer-lkhpbg" title="image-intersection-observer" />

The source code is also available on GitHub.

Prerequisites

The knowledge of Next.js and the localStorage API is required to get the most out of this article. We will also use the react-intersection-observer library to create the demo, so familiarity with it will be helpful.

Getting Started

We create a new Next.js application by running the command below in our terminal.

1npx create-next-app next-image-intersection-observer-app

Next, we navigate into the project directory.

1cd next-image-intersection-observer-app

Then run the code below to install react-intersection-observer.

1npm install react-intersection-observer

What we are building

We are building an image tracking application. As the user scrolls through an application, we track and increase an impression count when the user scrolls past a particular image.

We will also add a visual indicator to the image to inform us when it is in view.

This demo is similar to an analytics system that tracks impressions.

Adding the Content

Update the index.js file with the code below:

1import Image from "next/image";
2export default function Home() {
3 return (
4 <div className="w-full h-full flex flex-col justify-center items-center px-10 pt-10 bg-zinc-900 relative">
5
6 <h1 className="text-4xl font-bold text-white mb-16 text-center">
7 Next.js Intersection Observer Article
8 </h1>
9
10 {Array.from(Array(3).keys()).map((i) => (
11 <p className="text-white text-2xl mb-16" key={i}>
12 Irure pariatur velit est anim ipsum anim aliquip officia velit
13 consectetur. Duis sint ut consectetur ea anim. Sit proident culpa
14 velit officia do incididunt Lorem in deserunt non adipisicing occaecat
15 magna. Occaecat occaecat esse excepteur consequat occaecat cupidatat
16 aliquip labore esse ad ea. Laboris id excepteur nisi voluptate sunt
17 anim commodo amet reprehenderit.
18 </p>
19 ))}
20
21 <div className="mb-10">
22 <Image
23 src="/images/1.png"
24 width={300}
25 height={400}
26 alt="something amazing"
27 />
28 </div>
29
30 {Array.from(Array(5).keys()).map((i) => (
31 <p className="text-white text-2xl mb-16" key={i}>
32 Irure pariatur velit est anim ipsum anim aliquip officia velit
33 consectetur. Duis sint ut consectetur ea anim. Sit proident culpa
34 velit officia do incididunt Lorem in deserunt non adipisicing occaecat
35 magna. Occaecat occaecat esse excepteur consequat occaecat cupidatat
36 aliquip labore esse ad ea. Laboris id excepteur nisi voluptate sunt
37 anim commodo amet reprehenderit.
38 </p>
39 ))}
40
41 </div>
42 );
43}

Here, we create an array, loop through some dummy text, and add the image between the text.

Integrating react-intersection-observer

react-intersection-observer is an implementation of the Intersection Observer API for React. It informs us when an element enters or leaves the viewport.

It provides a useInView hook we can use to monitor the inView state of components. There is also a custom InView component, which we will be using. We modify the index.js file to include these with:

1import Image from "next/image";
2import { InView } from "react-intersection-observer";
3
4export default function Home() {
5 return (
6 <div className="w-full h-full flex flex-col justify-center items-center px-10 pt-10 bg-zinc-900 relative">
7 <h1 className="text-4xl font-bold text-white mb-16 text-center">
8 Next.js Intersection Observer Article
9 </h1>
10
11 {Array.from(Array(3).keys()).map((i) => (
12 <p className="text-white text-2xl mb-16" key={i}>
13 Irure pariatur velit est anim ipsum anim aliquip officia velit
14 consectetur....
15 </p>
16 ))}
17
18 <InView threshold={0.6}>
19 {({ ref, inView }) => (
20 <div className="mb-10" ref={ref}>
21 <Image
22 src="/images/1.png"
23 width={300}
24 height={400}
25 alt="something amazing"
26 />
27 <p
28 className={`text-white text-2xl font-bold ${
29 inView ? "text-green-600" : "text-red-600"
30 }`}
31 >{`Image in view? ${inView} `}</p>
32 </div>
33 )}
34 </InView>
35
36 {Array.from(Array(5).keys()).map((i) => (
37 <p className="text-white text-2xl mb-16" key={i}>
38 Irure pariatur velit est anim ipsum anim aliquip officia velit
39 consectetur....
40 </p>
41 ))}
42
43 </div>
44 );
45}

In the code above, we do the following:

  • Import InView from react-intersection-observer.

  • Wrap our image with InView using the render props method.

  • We have access to an inView boolean in the render props, and we use it to add a visual indicator. The indicator informs us when the image is in view. We change the text color to green when the image is visible and red when it’s not. We also change the content of the indicator’s text based on the inView state.

Setting up the Impression Counter

Having integrated react-intersection-observer and can track when the image is in view, let’s set up the functionality for the impression counter. Updating the index.js file, we have:

1import { useState } from "react";
2import Image from "next/image";
3import { InView } from "react-intersection-observer";
4
5function ImpressionCounter(inView, count, setCount) {
6 if (inView) {
7 setCount((count) => count + 1);
8 localStorage.setItem("impressionCount", count);
9 }
10}
11export default function Home() {
12 const [count, setCount] = useState(0);
13
14 return (
15 <div className="w-full h-full flex flex-col justify-center items-center px-10 pt-10 bg-zinc-900 relative">
16
17 <h2 className="text-4xl font-bold text-white mb-16 fixed top-10 right-10">
18 Impression Count: {count}
19 </h2>
20
21 {Array.from(Array(3).keys()).map((i) => (
22 <p className="text-white text-2xl mb-16" key={i}>
23 Irure pariatur velit est anim ipsum anim aliquip officia velit
24 consectetur...
25 </p>
26 ))}
27
28 <InView
29 threshold={0.6}
30 onChange={(inView) => ImpressionCounter(inView, count, setCount)}
31 >
32 {({ ref, inView }) => (
33 <div className="mb-10" ref={ref}>
34 <Image
35 src="/images/1.png"
36 width={300}
37 height={400}
38 alt="something amazing"
39 />
40 <p
41 className={`text-white text-2xl font-bold ${
42 inView ? "text-green-600" : "text-red-600"
43 }`}
44 >{`Image in view? ${inView} `}</p>
45 </div>
46 )}
47 </InView>
48
49 {Array.from(Array(5).keys()).map((i) => (
50 <p className="text-white text-2xl mb-16" key={i}>
51 Irure pariatur velit est anim ipsum anim aliquip officia velit
52 consectetur...
53 </p>
54 ))}
55
56 </div>
57 );
58}

In the code above, we do the following:

  • Create a count state using the useState hook. count will hold the impression count of the image, which we want to store. We set its default value to 0.

  • Create an ImpressionCounter function. It takes in inView, count, and setCount as arguments. We check if inView is true, and if so, increase the count by 1 and save that new count to localStorage.

  • To call ImpressionCounter, we pass it to InView's onChange method. onChange will run and call ImpressionCounter only when the inView state changes.

  • Display the total count in the h2 tag so people can easily know how many impressions the image has.

Here’s what the final page looks like. Notice the increasing impression count.

Conclusion

In this article, we learned how to track image impressions using Next.js and react-intersection-observer and store those impressions in our browser’s localStorage.

Further Reading

React Intersection Observer React Intersection Observer - Multiple Observers

Emadamerho-Atori Nefe

Frontend Developer and Technical Writer