11

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?

troglodyte07
  • 3,598
  • 10
  • 42
  • 66
  • 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. – Jamie Dixon Mar 19 '20 at 10:23
  • 1
    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. – Jamie Dixon Mar 19 '20 at 10:27

3 Answers3

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;
}
troglodyte07
  • 3,598
  • 10
  • 42
  • 66
4

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/

Daher
  • 1,001
  • 10
  • 10
1

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.

Shah
  • 2,126
  • 1
  • 16
  • 21
  • 1
    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 https://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. – myverdict Feb 10 '23 at 22:37
  • 1
    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) }` – Константин Ушаков Mar 16 '23 at 05:06
  • That’s a great idea! – Shah Mar 16 '23 at 05:20