Unable to access current property of ForwardedRef<HTMLDivElement>

Viewed 156

I'm trying to create a custom vertical scroll indicator, i.e. the vertical scrollbar will be hidden, and instead I will have up/down arrows appear if scrolling is available. My idea is to have a parent component where I create an useRef hook that hooks into a scrollable div. Then I pass the ref to a child component that will determine when to display the arrows based on the parent's ref.current.scrollHeight vs clientHeight. In the child component, I have a custom hook that takes in the ref and an object of triggers that determine when to remeasure the scrollHeight and clientHeight. I run into a type error where its trying to access the current property when ref is null. More details below. I'm never expecting the ref to be null, as it should be referencing a HTMLDivElement. How do I handle this null issue? Also, is my understanding of forwardRef correct? I'm expecting the child can access the parent ref's current properties. If not, then I guess this whole thing is moot.

The first section is my parent component. You can see on line 24 I have declared my ref as scrollContainerRef. It is referencing the div on line 31. The ref is also being passed into the OverflowIndication child component on line 39. The lines are commented in the code

import React, { useState, useRef, useCallback } from 'react';
import { ReadInstanceITF } from '../../../interfaces/interface';
import ReadInstanceHeader from './ReadInstanceHeader';
import ReadInstanceReadEntry from './ReadInstanceReadEntry';
import OverflowIndication from './OverflowIndication';


interface ReadInstancePropsITF {
  readInstance: ReadInstanceITF;
  isEdit: boolean;
  editTimer: number;
  isExpanded: boolean;
  expandTimer: number;
  handleIsExpanded: Function;
  handleUpdateReaderBook: Function;
};

const ReadInstance = ({ readInstance, isEdit, editTimer, isExpanded, expandTimer, handleIsExpanded, handleUpdateReaderBook } : ReadInstancePropsITF) => {
  const readEntryList = readInstance.read_entry || [];
  const [ readEntrySelectToggle, setReadEntrySelectToggle ] = useState(false); //triggers overflow check whenever readEntry is clicked.
  const readEntryListAppendTimer = 500;
  const readEntrySelectTimer = 300;
  //***** line 24 below *****
  const scrollContainerRef = useRef<HTMLDivElement>(null); 

  const handleReadEntrySelectToggle = useCallback(() => setReadEntrySelectToggle(readEntrySelectToggle => !readEntrySelectToggle), []);

  return (
    <div className='relative h-full w-full overflow-hidden'>
      //***** line 31 below *****
      <div ref={scrollContainerRef} className='h-full overflow-y-scroll scrollbar-hide'>

        <ReadInstanceHeader daysRead={readInstance.days_read} daysTotal={readInstance.days_total} isExpanded={isExpanded} expandTimer={expandTimer} handleIsExpanded={handleIsExpanded} />

        <ReadInstanceReadEntry readEntryList={readEntryList} readerBookId={readInstance.reader_book_id} readInstanceId={readInstance.ri_id} readEntryListAppendTimer={readEntryListAppendTimer} readEntrySelectTimer={readEntrySelectTimer } isEdit={isEdit} editTimer={editTimer} handleReadEntrySelectToggle={handleReadEntrySelectToggle} handleUpdateReaderBook={handleUpdateReaderBook} />

      </div>
        //***** line 39 below *****
      <OverflowIndication ref={scrollContainerRef} isEdit={isEdit} isExpanded={isExpanded} expandTimer={expandTimer} readEntryListLength={readEntryList.length} readEntryListAppendTimer={readEntryListAppendTimer} readEntrySelectToggle={readEntrySelectToggle} readEntrySelectTimer={readEntrySelectTimer} />


    </div>
  )
};

export default ReadInstance;

In the OverflowIndication child component, I believe forwardRef will allow access to the parent ref and I pass it to my custom hook useYOverflow on line 27.

import React, { forwardRef } from 'react';
import useYOverflow from '../../../hooks/useYOverflow';
import { BsChevronDown, BsChevronUp } from 'react-icons/bs';

interface OverflowIndicationPropsITF {
  isEdit: boolean;
  isExpanded: boolean;
  expandTimer: number;
  readEntryListLength: number;
  readEntryListAppendTimer: number;
  readEntrySelectToggle: boolean;
  readEntrySelectTimer: number;
};

const OverflowIndication = forwardRef<HTMLDivElement, OverflowIndicationPropsITF>(({ isEdit, isExpanded, expandTimer, readEntryListLength, readEntryListAppendTimer, readEntrySelectToggle, readEntrySelectTimer }, ref) => {

  const overflowTriggers = {
    isEdit,
    isExpanded,
    expandTimer,
    readEntryListLength,
    readEntryListAppendTimer,
    readEntrySelectToggle,
    readEntrySelectTimer
  };
    //***** line 27 below *****
  const { refYOverflowing, refYScrollBegin, refYScrollEnd } = useYOverflow({ref, overflowTriggers});

  return (
    <div>
      {!refYScrollBegin && <BsChevronUp className='absolute w-full top-0' />}
      {!refYScrollEnd && refYOverflowing && <BsChevronDown className='absolute w-full bottom-0' />}
    </div>
  )
});

export default OverflowIndication;

My custom hook useYOverflow is where my typescript issues are appearing. I get this error when I try to access ref.current

Property 'current' does not exist on type 'RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | MutableRefObject<HTMLDivElement | null>'.
  Property 'current' does not exist on type '(instance: HTMLDivElement | null) => void'.

Error Screenshot

Below is useYOverflow hook. Only the first useEffect is relevant. The other useEffects are handling the rest of the remeasuring triggers.

import { useState, useEffect, RefObject, ForwardedRef } from "react";


interface useYOverflowITF {
  ref: RefObject<HTMLDivElement> | ForwardedRef<HTMLDivElement>;
  overflowTriggers: {
    isExpanded?: boolean;
    expandTimer?: number;
    readEntryListLength?: number;
    readEntryListAppendTimer?: number;
    readEntrySelectToggle?: boolean;
    readEntrySelectTimer?: number;
    isEdit?: boolean;
    newAuthorList?: string[];
    deleteAuthorList?: string[];
  };
}

interface useYOverflowReturnITF {
  refYOverflowing: boolean;
  refYScrollBegin: boolean;
  refYScrollEnd: boolean;
}

const useYOverflow = ({ref, overflowTriggers}: useYOverflowITF): useYOverflowReturnITF => {
    const [refYOverflowing, setRefYOverflowing] = useState(false);
    const [refYScrollBegin, setRefYScrollBegin] = useState(true);
    const [refYScrollEnd, setRefYScrollEnd] = useState(false);

    useEffect(() => {
      if (!ref && !ref!.current) return
      const isYOverflowing = ref.current.scrollHeight > ref.current.clientHeight;
      setRefYOverflowing(isYOverflowing);

      const handleScroll = () => {
        const offsetBottom = ref?.current?.scrollHeight! - ref?.current?.clientHeight!;
        (ref?.current?.scrollTop! >= offsetBottom && refYScrollEnd === false) ? setRefYScrollEnd(true) : setRefYScrollEnd(false);
        (ref?.current?.scrollTop === 0) ? setRefYScrollBegin(true) : setRefYScrollBegin(false);
      }

      ref.current.addEventListener('scroll', handleScroll);
      return () => ref.current?.removeEventListener('scroll', handleScroll);
    }, []);

    //delay scroll height measurement until end of readInstance expand transition
    useEffect(() => {
      //need to add extra time to handle cases where scrollHeight is almost the height of the clientHeight.
      const expandTimer = overflowTriggers.expandTimer ? overflowTriggers.expandTimer + 100 : 0;
      const scrollHeightMeasureDelay = setTimeout(() => {
        const isYOverflowing = ref.current && ref.current.scrollHeight > ref.current.clientHeight || false;
        setRefYOverflowing(isYOverflowing);
      }, expandTimer);

      return () => clearTimeout(scrollHeightMeasureDelay);
    }, [overflowTriggers.isExpanded]);

    //delay scroll height measurement until end of ReadEntry enter or exit transition
    useEffect(() => {
      const scrollHeightMeasureDelay = setTimeout(() => {
        const isYOverflowing = ref.current && ref.current.scrollHeight > ref.current.clientHeight || false;
        setRefYOverflowing(isYOverflowing);
    }, overflowTriggers.readEntryListAppendTimer);

      return () => clearTimeout(scrollHeightMeasureDelay);
    }, [overflowTriggers.readEntryListLength]);

    //delay scroll height measurement until end of isReadEntrySelected transition
    useEffect(() => {
      const scrollHeightMeasureDelay = setTimeout(() => {
        const isYOverflowing = ref.current && ref.current.scrollHeight > ref.current.clientHeight || false;
        setRefYOverflowing(isYOverflowing);
    }, overflowTriggers.readEntrySelectTimer);

    return () => clearTimeout(scrollHeightMeasureDelay);
  }, [overflowTriggers.readEntrySelectToggle]);

    //delay scroll height measurement in edge case: isExpand: true, isEdit: true, >= 8 readEntry with all Delete Containers open and then changing isEdit to false.
    useEffect(() => {
      const scrollHeightMeasureDelay = setTimeout(() => {
        const isYOverflowing = ref.current && ref.current.scrollHeight > ref.current.clientHeight || false;
        setRefYOverflowing(isYOverflowing);
      }, overflowTriggers.readEntrySelectTimer);

      return () => clearTimeout(scrollHeightMeasureDelay);
    }, [overflowTriggers.isEdit]);

    //check overflow when adding or removing authors from CardBack.
    useEffect(() => {
      const isYOverflowing = ref.current && ref.current.scrollHeight > ref.current.clientHeight || false;
      setRefYOverflowing(isYOverflowing);
    }, [overflowTriggers.newAuthorList, overflowTriggers.deleteAuthorList])

    return { refYOverflowing, refYScrollBegin, refYScrollEnd };
}

export default useYOverflow;
0 Answers
Related