Home > OS >  Intersection observer will not observe multiple refs
Intersection observer will not observe multiple refs

Time:11-17

Hello I am using a switch statement to serve particular components to a page In my next js project. The switch statement receives a payload which it loops through in order to derive what component to serve. These components have been imported dynamically and I now wish to use this dynamic importing along with the Intersection Observer to load components when they come in the viewport to decrease the Initial page load time and split up the chunks. I have incorporated a hook that uses the intersection observer along with use ref to try to replicate my idea. Now this works when I give the reference to one div and it observes the component coming into the viewport as expected, however when I add multiple refs to my divs, I still only get the one div being observed with the ref.

What am I doing wrong? I thought you could reference the same ref multiple times and just use .current to identify the current element being observed?

Switch Statement:

import React from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '@loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';

const PriorityCollection = loadable(
  () => import('@culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
  {
    resolveComponent: (components) => components.PriorityCollection
  }
);

const TravelWithUs = loadable(
  () => import('../../../components/trips/travel-with-us/travel-with-us'),
  {
    resolveComponent: (components) => components.TravelWithUs
  }
);

const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
  resolveComponent: (components) => components.TrustMessaging
});

const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
  resolveComponent: (components) => components.PressMessaging
});

const TripsChatBanner = loadable(
  () => import('../../../components/trips/chat-banner/chat-banner'),
  {
    resolveComponent: (components) => components.TripsChatBanner
  }
);

const HpFeaturedArticles = loadable(
  () => import('../home-page-featured-articles/home-page-featured-articles'),
  {
    resolveComponent: (components) => components.HpFeaturedArticles
  }
);

const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
  resolveComponent: (components) => components.InstagramSection
});

const EmailForm = loadable(() => import('../../../components/trips/email-form'));

const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));

export const IncludeComponent = ({ collections, reviewData, type }) => {
  const [containerRef, isVisible] = useOnScreen({
    root: null,
    rootMargin: '0px',
    threshold: 0.1
  });

  const instagramCollection = collections.filter((collection) => collection.type === 'instagram');

  const getComponents = () =>
    collections.map((el, i) => {
      switch (el.type) {
        case 'trips':
        case 'article':
          return (
            <PriorityCollection
              key={i}
              collections={[el]}
              tctEnv={getTCTEnv()}
              wishlistButton={<IconWishlistButton />}
            />
          );
        case 'reviews':
          return (
            <>
              <div ref={containerRef} id={i}></div>
              <ReviewsSection reviewData={reviewData} />
            </>
          );
        case 'instagram':
          return (
            <>
              <div ref={containerRef} id={i}></div>
              <InstagramSection collection={instagramCollection} />
            </>
          );
        case 'featured':
          return <PressMessaging />;
        case 'trust':
          return <TrustMessaging type={type} />;
        case 'featuredArticle':
          return <HpFeaturedArticles />;
        case 'email':
          return <EmailForm />;
        case 'chat':
          return <TripsChatBanner />;
        case 'travel':
          return <TravelWithUs type={type} />;
        default:
          return;
      }
    });

  return getComponents();
};

custom hook:

import { useEffect, useState, useRef } from 'react';

export const useOnScreen = (options): any => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState([]);

  const callbackFunction = (entries) => {
    const [entry] = entries;

    if (entry.isIntersecting)
      setIsVisible((oldArray) => [
        ...oldArray,
        isVisible.indexOf(entry.target.id) === -1 && entry.target.id !== undefined
          ? entry.target.id
          : console.log('nothing')
      ]);
  };

  useEffect(() => {
    const observer = new IntersectionObserver(callbackFunction, options);
    if (containerRef.current) observer.observe(containerRef.current);

    return () => {
      if (containerRef.current) observer.unobserve(containerRef.current);
    };
  }, [containerRef.current, options]);

  return [containerRef, isVisible];
};

Currently only the instagram ref gets observed

CodePudding user response:

If I understand your code correctly, more than one component is possibly rendered from getComponents. For instance, the tree could contain:

<div ref={containerRef} id={i}></div>
<ReviewsSection reviewData={reviewData} />
<div ref={containerRef} id={i}></div>
<InstagramSection collection={instagramCollection} />

And you want both divs there to be observed. It doesn't work because the ref doesn't trigger the effect by itself. The ref is simply an object like { current: null }. When the tree is rendered, containerRef.current is set to the first div, then it is set to the second div, then the effect runs.

To do what you want you can:

  1. Call the custom hook multiple times, and assign one containerRef to each div. The issue here is, of course, you will also have multiple IntersectionObservers instances.
  2. Declare multiple refs and pass them to the custom hook via argument, instead of returning the ref from the custom hook.
  3. Implement a callback ref that adds every div to a list, skipping duplicates. This one allows you to keep the same implementation in getComponents, but is also the trickiest for the hook.

CodePudding user response:

Solved with this:

import React, { useEffect, useReducer } from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '@loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';

const PriorityCollection = loadable(
  () => import('@culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
  {
    resolveComponent: (components) => components.PriorityCollection
  }
);

const TravelWithUs = loadable(
  () => import('../../../components/trips/travel-with-us/travel-with-us'),
  {
    resolveComponent: (components) => components.TravelWithUs
  }
);

const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
  resolveComponent: (components) => components.TrustMessaging
});

const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
  resolveComponent: (components) => components.PressMessaging
});

const TripsChatBanner = loadable(
  () => import('../../../components/trips/chat-banner/chat-banner'),
  {
    resolveComponent: (components) => components.TripsChatBanner
  }
);

const HpFeaturedArticles = loadable(
  () => import('../home-page-featured-articles/home-page-featured-articles'),
  {
    resolveComponent: (components) => components.HpFeaturedArticles
  }
);

const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
  resolveComponent: (components) => components.InstagramSection
});

const EmailForm = loadable(() => import('../../../components/trips/email-form'));

const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));

export const IncludeComponent = ({ collections, reviewData, type }) => {
  const [containerRef, isVisible] = useOnScreen({
    root: null,
    rootMargin: '0px',
    threshold: 0.1
  });

  const instagramCollection = collections.filter((collection) => collection.type === 'instagram');

  const getComponents = () =>
    collections.map((el, i) => {
      switch (el.type) {
        case 'trips':
        case 'article':
          return (
            <PriorityCollection
              key={i}
              collections={[el]}
              tctEnv={getTCTEnv()}
              wishlistButton={<IconWishlistButton />}
            />
          );
        case 'reviews':
          return (
            <>
              <div
                ref={(element) => {
                  containerRef.current[i] = element;
                }}
                id={i}
              ></div>
              {isVisible.indexOf(i.toString()) !== -1 && <ReviewsSection reviewData={reviewData} />}
            </>
          );
        case 'instagram':
          return (
            <>
              <div
                ref={(element) => {
                  containerRef.current[i] = element;
                }}
                id={i}
              ></div>
              <InstagramSection collection={instagramCollection} />
            </>
          );
        case 'featured':
          return <PressMessaging />;
        case 'trust':
          return <TrustMessaging type={type} />;
        case 'featuredArticle':
          return <HpFeaturedArticles />;
        case 'email':
          return <EmailForm />;
        case 'chat':
          return <TripsChatBanner />;
        case 'travel':
          return <TravelWithUs type={type} />;
        default:
          return;
      }
    });

  return getComponents();
};

hook:

import { useEffect, useState, useRef } from 'react';

export const useOnScreen = (options): any => {
  const containerRef = useRef<HTMLDivElement[]>([]);
  const [isVisible, setIsVisible] = useState([]);

  const callbackFunction = (entries) => {
    const [entry] = entries;

    if (entry.isIntersecting) {
      const checkIdInArray = isVisible.indexOf(entry.target.id) === -1;
      if (checkIdInArray) setIsVisible((oldArray) => [...oldArray, entry.target.id]);
    }
  };

  useEffect(() => {
    const observer = new IntersectionObserver(callbackFunction, options);
    if (containerRef.current)
      containerRef.current.forEach((el) => {
        observer.observe(el);
      });

    return () => {
      if (containerRef.current)
        containerRef.current.forEach((el) => {
          observer.unobserve(el);
        });
    };
  }, [containerRef, options]);

  return [containerRef, isVisible];
};
  • Related