Using Intersection Observer in Typescript causes a bug when utilized with useRef

Currently, I have a text animation that is functioning flawlessly. However, my goal now is to implement an Intersection Observer so that the animation only triggers when I scroll down to the designated box.

To achieve this, I utilized the useRef React hook as a reference point for the element I want to observe. I then applied this reference to my Box using ref={containerRef}. Subsequently, I defined a callback function that receives an array of IntersectionObserverEntries. Within this function, I extract the first entry and check if it intersects with the viewport. If it does, I call setIsVisible with the value of entry.isIntersecting (true/false). Following this, I incorporated the useEffect React hook and established an observer constructor by utilizing the previously created callback function and options. This logic was embedded in a custom hook dubbed useElementOnscreen.

Nevertheless, Typescript is flagging an error at containerRef?.current:

Argument of type 'IntersectionObserver' is not assignable to parameter of type 'Element'.
  Type 'IntersectionObserver' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 160 more.

Regrettably, I am unsure how to rectify this issue. It appears that this also leads to errors with ref={containerRef}.

The anticipated type originates from property 'ref' as declared here on type 'IntrinsicAttributes & { component: ElementType<any>; } & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<...> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>'

About the animation: The TopAnimateBlock and BottomAnimateBlock include properties such as numOfLine indicating the number of lines within the block. The delayTopLine property in BottomAnimateBlock should align with numOfLine in TopAnimateBlock since we need to wait for the top lines to animate firstly.

TextAnimation.tsx

import { Box, Stack, Typography } from '@mui/material';
import React, { useRef, useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';

const showTopText = keyframes`
  0% { transform: translate3d(0, 100% , 0); }
  40%, 60% { transform: translate3d(0, 50%, 0); }
  100% { transform: translate3d(0, 0, 0); }
`;
const showBottomText = keyframes`
  0% { transform: translate3d(0, -100%, 0); }
  100% { transform: translate3d(0, 0, 0); }
`;

const Section = styled.section`
  width: calc(100% + 10vmin);
  display: flex;
  flex-flow: column;
  padding: 2vmin 0;
  overflow: hidden;
  &:last-child {
    border-top: 1vmin solid white;
  }
`;

const Block = styled.div<{ numOfLine: number }>`
  position: relative;
`;
const TopAnimateBlock = styled(Block)`
  animation: ${showTopText} calc(0.5s * ${props => props.numOfLine}) forwards;
  animation-delay: 0.5s;
  transform: translateY(calc(100% * ${props => props.numOfLine}));
`;
const BottomAnimateBlock = styled(Block)<{ delayTopLine: number }>`
  animation: ${showBottomText} calc(0.5s * ${props => props.numOfLine}) forwards;
  animation-delay: calc(0.7s * ${props => props.delayTopLine});
  transform: translateY(calc(-100% * ${props => props.numOfLine}));
`;

const TextStyle = styled.p<{ color: string }>`
  font-family: Roboto, Arial, sans-serif;
  font-size: 12vmin;
  color: ${props => props.color};
`;


const useElementOnScreen = (options) => {
    const containerRef = useRef<IntersectionObserver | null>(null);
    const [isVisible, setIsVisible] = useState(false);

    const callbackFunction = (entries) => {
        const [entry] = entries;
        setIsVisible(entry.isIntersecting);
    };

    useEffect(() => {
        const observer = new IntersectionObserver(callbackFunction, options);

        if (containerRef.current) observer.observe(containerRef?.current);

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

    return [containerRef, isVisible];
};

export function Details() {
    const [containerRef, isVisible] = useElementOnScreen({
        root: null,
        rootMargin: '0px',
        threshold: 1.0,
    });

    return (
    <>
    <Typography>Scroll Down</Typography>
     <Box ref={containerRef}>
      <Section>
        <TopAnimateBlock numOfLine={2}>
          <TextStyle color="grey">mimicking</TextStyle>
          <TextStyle color="white">apple's design</TextStyle>
        </TopAnimateBlock>
      </Section>
      <Section>
        <BottomAnimateBlock numOfLine={1} delayTopLine={2}>
          <TextStyle color="white">for the win!</TextStyle>
        </BottomAnimateBlock>
      </Section>
    </Box>
</>
  );
};

Answer №1

After analyzing the code, I have identified two main issues:

The first issue lies in this particular line of code:

 const containerRef = useRef<IntersectionObserver | null>(null);

Here, the generic useRef is used with IntersectionObserver | null, suggesting that the reference container will either hold an instance of IntersectionObserver or null. However, the ref is intended to be used with a Box element (similar to a div in material-UI).

To rectify this, it is advisable to update the statement to:

  const containerRef = useRef<HTMLDivElement | null>(null);

The second issue arises from the undeclared return type of the hook which results in TypeScript inferring it as an array based on what is being returned ([containerRef, isVisible]). As a result, TypeScript interprets it as:

https://i.sstatic.net/mh8bU.png

(boolean | React.MutableRefObject<HTMLDivElement | null>)[]
. This misinterpretation occurs because the actual return type should be a tuple since both elements in the array are different types.

To resolve this issue and prevent TypeScript from raising errors, it is recommended to explicitly declare the return type when defining the hook.

const useOnScreen = <T,>(options : T): [MutableRefObject<HTMLDivElement | null>, boolean] => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const [isVisible, setIsVisible] = useState(false);

  const callbackFunction = (entries :IntersectionObserverEntry[]) => {
    const [entry] = entries;
    setIsVisible(entry.isIntersecting);
  };

  useEffect(() => {
    const observer = new IntersectionObserver(callbackFunction, options);

    if (containerRef.current) observer.observe(containerRef?.current);

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

  return [containerRef, isVisible];
};

For more information on the distinction between tuple return types and array return types, refer to this link.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The concatenation function in JavaScript does not seem to be functioning properly with JSON

My attempt to use .concat() in order to combine two objects is resulting in tiles.concat is not a function The following code (in an angular app and written in coffeescript): $scope.tiles = new UI(); $scope.tiles.loadUITiles(); console.log($sco ...

Using ReactJS to activate child components from a parent component

I am working on a parent component that functions as a Form in my project. export default class Parent extends Component{ submitInput(event){ ...calling Children components } render(){ ...

ReactJS initiated a GET request, which was met with a 302 redirect. However, the response was hinder

This is the technology stack I'm working with: Frontend server: Node + ReactJS (f.com) Backend server (API): Golang (b.com) In my development environment, I am using create-react-app to run the frontend server with the command npm start. I want to ...

"Exploring Typescript return types in relation to the .getElementsByClassName() method

I am struggling to understand why there is a difference in the results when I use getElementsByClassName on two distinct elements: Check out this code snippet: let section:HTMLElement = document.getElementById("mainSection"); // When I run this, it retu ...

Conceal the HTML element within a subscription

Currently, I am utilizing Angular and have a checkbox that I need to toggle visibility based on the response of an API call subscription. The issue lies in a delay when trying to hide the checkbox (as it is initially set to visible). My assumption is that ...

Having trouble getting an anchor tag with an onclick event handler to function properly in a React

The script doesn't seem to execute properly. <a href="/blog" onClick={this.props.selectPage} name="blog">Blog</a> When I eliminate href="/blog", the selectPage function works, but if I remove onClick={this.props.selectPage}, it redirects ...

The anchor link is not aligning properly due to the fluctuating page width

Seeking help to resolve an issue I'm facing. Maybe someone out there has a solution? The layout consists of a content area on the left (default width=70%) and a menu area on the right (default width=30%). When scrolling down, the content area expand ...

Update nested child object in React without changing the original state

Exploring the realms of react and redux, I stumbled upon an intriguing challenge - an object nested within an array of child objects, complete with their own arrays. const initialState = { sum: 0, denomGroups: [ { coins: [ ...

Different ways to incorporate the div element with text content as opposed to images in ngx-slick-carousel

I am looking to incorporate multiple <div> elements with content into the ngx-slick-carousel in my Angular 12 application. Currently, the carousel is displaying placeholder images and I am unsure of how to replace these images with div elements. Her ...

Why does my React app on GCP Storage show a 'Not Found' error when using 'Fetch as Google'?

I have successfully built my React app using create-react-app and then uploaded it to GCP Storage. I configured the website settings (Main page and 404 page) to index.html. This is the current folder structure: ├── asset-manifest.json ├── fav ...

Error in AngularJS/HTML input week formatting

Utilizing AngularJS, I have implemented an input week tag for users to select a week. However, I am encountering an issue with the format. According to my research, the format should be yyyy-W##, and I have followed this format but AngularJS is still show ...

Having Trouble with Form Submission in React with Formik and Yup

I'm currently utilizing the Autocomplete component from Material UI along with Formik and Yup for validation purposes. The issue I am facing is that upon submitting my values and checking the console, nothing appears, indicating a failure to meet the ...

Incorporate external input properties into Material UI's TextField component

I've integrated react-payment-inputs library for handling credit card input in my project. Using the provided getter props, I can easily manage credit card inputs like this: <input {...getCardNumberProps()} />. However, when applying the same a ...

Can asynchronous programming lead to memory leakage?

I'm wondering about the potential for memory leaks in asynchronous operations, specifically within Javascript used on both frontend and backend (node.js). When the execute operation is initiated, a delegate named IResponder is instantiated. This dele ...

What are the steps to resolve issues with my dropdown menu in IE9?

I have a cool CSS built hover-over drop-down menu that I want to add to my website. It works perfectly on all browsers except for IE9. Here is the code and stylesheet I am using: Check out the code and sheet here If anyone has any insights on what might ...

What method can be used to adjust the tailwind animation based on a specific value

If the value is true, I would like the attribute animation-slide-right to be given, and if the value is false, then the attribute animation-slide-left should be applied. Unfortunately, after the initial rendering, the animation does not happen when the va ...

The http-proxy-middleware feature does not pass along the entire path

I'm currently setting up BrowserSync to run in server mode and proxy my API requests to the backend on the same machine but on a different port. I am using http-proxy-middleware with Gulp to achieve this setup. BrowserSync is operating on port 8081, ...

I'm intrigued: what type of syntax is Facebook's polling service utilizing in the callback?

While monitoring the Network Monitor on Chrome's developer tool, I observed how Facebook updates content on their news feed. All AJAX responses start with the following: for (;;);{"__ar":1,"payload":[]} I'm curious about what the for(;;); piec ...

Issue with rendering a component using OnClick function in react-redux

I've been trying to figure out this issue for hours now. In my App component, I'm attempting to add a new list by calling a Redux action as a prop when a button is clicked. The action is supposed to push a new list into the array, but for some re ...

Leveraging the power of Dojo and Ajax to seamlessly integrate the html initialization process through the

I am dealing with a webpage that loads multiple sections via ajax. For example: <body> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true, isDebug:true"></s ...