How to check if a div is overflowing in react functional component
Asked Answered
G

3

13

I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways

1. Using ref

import React from "react";
import "./styles.css";

export default function App(props) {
  const [showMore, setShowMore] = React.useState(false);
  const onClick = () => {
    setShowMore(!showMore);
  };

  const checkOverflow = () => {
    const el = ref.current;
    const curOverflow = el.style.overflow;

    if ( !curOverflow || curOverflow === "visible" )
        el.style.overflow = "hidden";

    const isOverflowing = el.clientWidth < el.scrollWidth 
        || el.clientHeight < el.scrollHeight;

    el.style.overflow = curOverflow;

    return isOverflowing;
  };

  const ref = React.createRef();

  return (
    <>
      <div ref={ref} className={showMore ? "container-nowrap" : "container"}>
        {props.text}
      </div>
      {(checkOverflow()) && <span className="link" onClick={onClick}>
        {showMore ? "show less" : "show more"}
      </span>}
    </>
  )
}

2. Using forward ref

Child component

export const App = React.forwardRef((props, ref) => {
  const [showMore, setShowMore] = React.useState(false);
  const onClick = () => {
    setShowMore(!showMore);
  };

  const checkOverflow = () => {
    const el = ref.current;
    const curOverflow = el.style.overflow;

    if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";

    const isOverflowing =
      el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;

    el.style.overflow = curOverflow;

    return isOverflowing;
  };

  return (
    <>
      <div ref={ref} className={showMore ? "container-nowrap" : "container"}>
        {props.text}
      </div>
      {checkOverflow() && (
        <span className="link" onClick={onClick}>
          {showMore ? "show less" : "show more"}
        </span>
      )}
    </>
  );
});

Parent component

import React from "react";
import ReactDOM from "react-dom";

import { App } from "./App";

const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
  <React.StrictMode>
    <App
      ref={ref}
      text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
    />
  </React.StrictMode>,
  rootElement
);

But I got the following error in both approaches - Cannot read property 'style' of null. What am I doing wrong? How can I achieve what I want?

Guise answered 19/3, 2020 at 10:19 Comment(2)
The first time you render your component, the ref will not be set yet. Only after that first render will ref.current have a value.Hibernal
I don't have a good answer for this because I'm not entirely sure what the best practice is, but one thing I've done in my app for a similar situation is initially set showMore to null and have useLayoutEffect with an empty dependency list [] where I then do setShowMore(false) to trigger the re-render where ref.current will be available.Hibernal
G
13

As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code

Component

import React from "react";
import "./styles.css";

export default function App(props) {
  const ref = React.createRef();
  const [showMore, setShowMore] = React.useState(false);
  const [showLink, setShowLink] = React.useState(false);

  React.useLayoutEffect(() => {
    if (ref.current.clientWidth < ref.current.scrollWidth) {
      setShowLink(true);
    }
  }, [ref]);

  const onClickMore = () => {
    setShowMore(!showMore);
  };

  return (
    <div>
      <div ref={ref} className={showMore ? "" : "container"}>
        {props.text}
      </div>
      {showLink && (
        <span className="link more" onClick={onClickMore}>
          {showMore ? "show less" : "show more"}
        </span>
      )}
    </div>
  );
}

CSS

.container {
  overflow-x: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: 200px;
}

.link {
  text-decoration: underline;
  cursor: pointer;
  color: #0d6aa8;
}
Guise answered 19/3, 2020 at 14:55 Comment(0)
C
6

We could create a custom hooks to know if we have overflow.

import * as React from 'react';

const useIsOverflow = (ref, isVerticalOverflow, callback) => {
  const [isOverflow, setIsOverflow] = React.useState(undefined);

  React.useLayoutEffect(() => {
    const { current } = ref;
    const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;

    const trigger = () => {
      const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;

      setIsOverflow(hasOverflow);

      if (callback) callback(hasOverflow);
    };

    if (current) {
      trigger();
    }
  }, [callback, ref, isVerticalOverflow]);

  return isOverflow;
};

export default useIsOverflow;

and just check in your component

import * as React from 'react';

import { useIsOverflow } from './useIsOverflow';

const App = () => {
  const ref = React.useRef();
  const isOverflow = useIsOverflow(ref);

  console.log(isOverflow);
  // true

  return (
    <div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
      <div style={{ height: '200px' }}>Hello React</div>
    </div>
  );
};

Thanks to Robin Wieruch for his awesome articles https://www.robinwieruch.de/react-custom-hook-check-if-overflow/

Cliftonclim answered 29/1, 2022 at 21:29 Comment(0)
B
2

Solution using TS and Hooks

Create your custom hook:

import React from 'react'

interface OverflowY {
  ref: React.RefObject<HTMLDivElement>
  isOverflowY: boolean
}

export const useOverflowY = (
  callback?: (hasOverflow: boolean) => void
): OverflowY => {
  const [isOverflowY, setIsOverflowY] = React.useState(false)
  const ref = React.useRef<HTMLDivElement>(null)

  React.useLayoutEffect(() => {
    const { current } = ref

    if (current && hasOverflowY !== isOverflowY) {
      const hasOverflowY = current.scrollHeight > window.innerHeight
      // RHS of assignment could be current.scrollHeight > current.clientWidth
      setIsOverflowY(hasOverflowY)
      callback?.(hasOverflowY)
    }
  }, [callback, ref])

  return { ref, isOverflowY }
}

use your hook:

const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code

Import your files as need be and update code to your needs.

Bunco answered 16/6, 2022 at 19:48 Comment(3)
Thank you for his solution. The useLayoutEffect gives me an error in the terminal even though the UI works correctly. The error led me to this link gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 and I used the first option to eliminate the error, i.e., using a useEffect in place of useLayoutEffect and the terminal error goes away and the UI still works correctly.Tillion
It's not best practice to place setIsOverflowY(hasOverflowY) callback?.(hasOverflowY) in every update. Better to make something like if (hasOverflowY !== isOverflowY) { setIsOverflowY(hasOverflowY) callback?.(hasOverflowY) }Headsail
That’s a great idea!Bunco

© 2022 - 2024 — McMap. All rights reserved.