How to prevent undefined issue when attaching event on DOM elements created by slick slider using Intersection or mutation observer in NextJs

Create a next app

npx create-next-app slick-slider-demo

Following is the folder structure for your refernce

Now install react slick slider

npm i react-slick --save
//for css
npm install slick-carousel --save

Now package.json is as follows

{
  "name": "slick-slider-demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "14.1.4",
    "react": "^18",
    "react-dom": "^18",
    "react-slick": "^0.30.2",
    "slick-carousel": "^1.8.1"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "@types/react-slick": "^0.23.13",
    "eslint": "^8",
    "eslint-config-next": "14.1.4",
    "typescript": "^5"
  }
}

Update global styles as follows

// src/styles/globals.css
.image-slider-container {
  margin: auto;
  width: 50vw;
}

.image-slider-container .slick-prev:before,
.image-slider-container .slick-next:before {
  color: black;
}

Now update app file

// src/pages/_app.tsx
import "@/styles/globals.css";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

Now make api for tagging purpose which can basically track user clicks

// src/pages/api/t.ts
// Next.js API route support: 
// https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  message: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const { tagging } = req.body;
  // You can save the tagging data in DB or anywhere for analytics
  // purpose
  res.status(200).json({ message: "Tagging successful!" });
}

Now we will make a page rendering in base route which will basically load multiple slick sliders in a page in lazy load fashion with help of dynamic routing

// src/pages/index.tsx
import Head from "next/head";
import React from "react";
import Loader from "../components/Loader";
import LazyLoadComponent from "../components/LazyLoadComponent";
import dynamic from "next/dynamic";

const SliderComponent = dynamic(() => import("../components/SliderComponent"), {
  loading: () => <Loader />,
});

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev1"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev2"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev3"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev4"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev5"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev6"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev7"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev8"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev9"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev10"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev11"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev12"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev13"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev14"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev15"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev16"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev17"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev18"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev19"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev20"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev21"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev22"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev23"}/>} />
      <LazyLoadComponent  Component={<SliderComponent addClass={"dev24"}/>} />

    </>
  );
}

We will make useMount hook to check if component is mounted

// src/hooks/useMount.ts
import { useEffect, useState } from "react";

const useMount = () => {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  }, []);
  return { hasMounted };
};

export default useMount;

Our Loader component with its modular style is as follows

// src/components/Loader/index.tsx
import React from 'react'
import styles from "./style.module.css"

const Loader = () => {
  return (
    <div className={styles.loader}></div>
  )
}

export default Loader
// src/components/Loader/style.module.css
.loader{
    height:100px;
    width:100px;
    border-top:3px solid purple;
    border-radius:50%;
    animation: spinner 1s linear infinite;
  }

  @keyframes spinner{
    100%{
    transform:rotate(360deg)
    }
  }

Our Lazyload higher order component is as follows

// src/components/LazyLoadComponent/index.tsx
//src/components/LazyLoadComponent/index.jsx
import React, { useEffect, useRef, useState } from "react";
import Load from "../Loader";

const options = {
  root: null,
  rootMargin: "0px",
  threshold: 0,

  /* required options*/
  trackVisibility: true,
  delay: 100,
};

const LazyLoadComponent = ({ Component, Loader = Load}: any) => {
  const targetRef: any = useRef();
  const [showComponent, setShowComponent] = useState(false);
  const [minHeight, setMinHeight] = useState("50vh");

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          //this ensures that once component is
          //loaded lazily it does not need to be
          //re rendered if the user scrolls away
          //and this component gets out of viewport set in options
          setShowComponent(true);
          setMinHeight("1px");
        }
      });
    }, options);

    observer.observe(targetRef.current);

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div style={{ minHeight }} ref={targetRef}>
      {showComponent ? Component  : <Loader />}
    </div>
  );
};

export default LazyLoadComponent;

I have explained in deep about my lazy loading implementation in this blog for your reference - Lazy load implementation

Now we will make slider component in which we will attach the click event on previous and next slick arrows and call tagging api to maintain records of clicks by users visiting this page analyze for future use. Here we will make use of mutation observer also to handle situation where there is delay in creation of previous and next button of slick slider and ensure that our click event is attached properly for such scenarios.

// src/components/SliderComponent/index.tsx
import React, { useEffect } from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import useMount from "@/hooks/useMount";

const SliderComponent = ({ addClass = "" }) => {
  const { hasMounted } = useMount();

  useEffect(() => {
    if (hasMounted) {
      const observerOptions = {
        childList: true,
        attributes: true,
      };

      const targetNode: any = document.querySelector(
        `.${addClass} .slick-arrow.slick-prev`
      );

      const callback1 = () => {
        targetNode?.addEventListener("click", () => {
          (async () => {
            await fetch("http://localhost:3000/api/t", {
              method: "post",
              body: JSON.stringify({
                tagging: "previous slick arrow clicked",
              }),
            });
          })();
          observer.disconnect();
        });
      };
      const observer = new MutationObserver(callback1);
      if (targetNode) {
        //this will execute if slick slider arrows element are created as soon as component is mounted
        callback1();
      } else {
        //this will execute if slick slider arrows element are created after some delay so it can observe that mutation in DOM
        observer.observe(targetNode, observerOptions);
      }
      const targetNode2: any = document.querySelector(
        `.${addClass} .slick-arrow.slick-next`
      );
      const callback2 = () => {
        targetNode2?.addEventListener("click", () => {
          (async () => {
            await fetch("http://localhost:3000/api/t", {
              method: "post",
              body: JSON.stringify({ tagging: "next slick arrow clicked" }),
            });
          })();
          observer2.disconnect();
        });
      };
      const observer2 = new MutationObserver(callback2);
      if (targetNode2) {
        callback2();
      } else {
        observer2.observe(targetNode2, observerOptions);
      }
    }
  }, [hasMounted]);

  const settings = {
    className: "center",
    centerMode: true,
    infinite: true,
    centerPadding: "60px",
    slidesToShow: 3,
    speed: 500,
  };
  return (
    <div className={`${addClass} image-slider-container`}>
      <Slider {...settings}>
        <div>
          <img
            style={{ height: "100px", width: "100px" }}
            src="https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?auto=compress&cs=tinysrgb&w=600"
          />
        </div>
        <div>
          <img
            style={{ height: "100px", width: "100px" }}
            src="https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?auto=compress&cs=tinysrgb&w=600"
          />
        </div>
        <div>
          <img
            style={{ height: "100px", width: "100px" }}
            src="https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?auto=compress&cs=tinysrgb&w=600"
          />
        </div>
        <div>
          <img
            style={{ height: "100px", width: "100px" }}
            src="https://images.pexels.com/photos/674010/pexels-photo-674010.jpeg?auto=compress&cs=tinysrgb&w=600"
          />
        </div>
      </Slider>
    </div>
  );
};

export default SliderComponent;

Our implementation is complete and we have also implemented slick slider with tagging api with intersection observer and handled undefined target element using mutation observer.

Hope you like the tutorial and find it helpful. Follow me for such exciting and beneficial articles in future.

Happy coding!