Jemar Jones
Jemar Jones

Reputation: 1645

How to detect overflow of React component without ReactDOM?

Basically i want to be able to detect if a react component has children which are overflowing. Just as in this question. I have found that the same thing is possible using ReactDOM, however i cannot/should not use ReactDOM. I don't see anything on the suggested alternative,ref, that is equivalent.

So what i need to know is if it is possible to detect overflow within a react component under these conditions. And to the same point, is it possible to detect width at all?

Upvotes: 22

Views: 75126

Answers (6)

afalak
afalak

Reputation: 521

The same could be achieved using React hooks:

The first thing you need would be a state which holds boolean values for text open and overflow active:

const [textOpen, setTextOpen] = useState(false);
const [overflowActive, setOverflowActive] = useState(false);

Next, you need a ref on the element you want to check for overflowing:

const textRef = useRef();
<p ref={textRef}>
    Some huuuuge text
</p>

The next thing is a function that checks if the element is overflowing:

function isOverflowActive(event) {
    return event.offsetHeight < event.scrollHeight || event.offsetWidth < event.scrollWidth;
}

Then you need a useEffect hook that checks if the overflow exists with the above function:

useEffect(() => {
    if (isOverflowActive(textRef.current)) {
        setOverflowActive(true);
        return;
    }

    setOverflowActive(false);
}, [isOverflowActive]);

And now with those two states and a function that checks the existence of an overflowing element, you can conditionally render some element (eg. Show more button):

{!textOpen && !overflowActive ? null : (
    <button>{textOpen ? 'Show less' : 'Show more'}</button>
)}

Upvotes: 9

Kraw24
Kraw24

Reputation: 183

I needed to achieve this in React TypeScript, as such here is the updated solution in TypeScript using React Hooks. This solution will return true if there are at least 4 lines of text.

We declare the necessary state variables:

  const [overflowActive, setOverflowActive] = useState<boolean>(false);
  const [showMore, setShowMore] = useState<boolean>(false);

We declare the necessary ref using useRef:

  const overflowingText = useRef<HTMLSpanElement | null>(null);

We create a function that checks for overflow:

  const checkOverflow = (textContainer: HTMLSpanElement | null): boolean => {
    if (textContainer)
      return (
        textContainer.offsetHeight < textContainer.scrollHeight || textContainer.offsetWidth < textContainer.scrollWidth
      );
    return false;
  };

Lets build a useEffect that will be called when overflowActive changes and will check our current ref object to determine whether the object is overflowing:

  useEffect(() => {
    if (checkOverflow(overflowingText.current)) {
      setOverflowActive(true);
      return;
    }

    setOverflowActive(false);
  }, [overflowActive]);

In our component's return statement, we need to bind the ref to an appropriate element. I am using Material UI coupled with styled-components so the element in this example will be StyledTypography:

<StyledTypography ref={overflowingText}>{message}</StyledTypography>

Styling the component in styled-components:

const StyledTypography = styled(Typography)({
  display: '-webkit-box',
  '-webkit-line-clamp': '4',
  '-webkit-box-orient': 'vertical',
  overflow: 'hidden',
  textOverflow: 'ellipsis',
});

Upvotes: 14

Tuval Rotem
Tuval Rotem

Reputation: 101

To anyone who wonder how it can be done with hooks and useRef:

// This is custom effect that calls onResize when page load and on window resize
const useResizeEffect = (onResize, deps = []) => {
  useEffect(() => {
    onResize();
    window.addEventListener("resize", onResize);

    return () => window.removeEventListener("resize", onResize);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [...deps, onResize]);
};

const App = () => {
  const [isScrollable, setIsScrollable] = useState(false);
  const [container, setContainer] = useState(null);
  // this has to be done by ref so when window event resize listener will trigger - we will get the current element
  const containerRef = useRef(container);
  containerRef.current = container;
  const setScrollableOnResize = useCallback(() => {
    if (!containerRef.current) return;
    const { clientWidth, scrollWidth } = containerRef.current;
    setIsScrollable(scrollWidth > clientWidth);
  }, [containerRef]);
  useResizeEffect(setScrollableOnResize, [containerRef]);

  return (
    <div
      className={"container" + (isScrollable ? " scrollable" : "")}
      ref={(element) => {
        if (!element) return;
        setContainer(element);
        const { clientWidth, scrollWidth } = element;
        setIsScrollable(scrollWidth > clientWidth);
      }}
    >
      <div className="content">
        <div>some conetnt</div>
      </div>
    </div>
  );
};

Edit bv7q6

Upvotes: 4

Rinor
Rinor

Reputation: 1999

The implementation of the solution proposed by @Jemar Jones:

export default class OverflowText extends Component {
  constructor(props) {
    super(props);
    this.state = {
      overflowActive: false
    };
  }

  isEllipsisActive(e) {
    return e.offsetHeight < e.scrollHeight || e.offsetWidth < e.scrollWidth;
  }

  componentDidMount() {
    this.setState({ overflowActive: this.isEllipsisActive(this.span) });
  }

  render() {
    return (
      <div
        style={{
          width: "145px",
          textOverflow: "ellipsis",
          whiteSpace: "nowrap",
          overflow: "hidden"
        }}
        ref={ref => (this.span = ref)}
      >
        <div>{"Triggered: " + this.state.overflowActive}</div>
        <span>This is a long text that activates ellipsis</span>
      </div>
    );
  }
}

Edit 1o0k7vr1m3

Upvotes: 15

Jemar Jones
Jemar Jones

Reputation: 1645

In addition to @jered's excellent answer, i'd like to mention the qualifier that a ref will only return an element that directly has access to the various properties of regular DOM elements if the ref is placed directly on a DOM element. That is to say, it does not behave in this way with Components.

So if you are like me and have the following:

var MyComponent = React.createClass({
  render: function(){
    return <SomeComponent id="my-component" ref={(el) => {this.element = el}}/>
  }
})

and when you attempt to access DOM properties of this.element (probably in componentDidMount or componentDidUpdate) and you are not seeing said properties, the following may be an alternative that works for you

var MyComponent = React.createClass({
  render: function(){
    return <div ref={(el) => {this.element = el}}>
             <SomeComponent id="my-component"/>
          </div>
  }
})

Now you can do something like the following:

componentDidUpdate() {
  const element = this.element;
  // Things involving accessing DOM properties on element
  // In the case of what this question actually asks:
  const hasOverflowingChildren = element.offsetHeight < element.scrollHeight ||
                                 element.offsetWidth < element.scrollWidth;
},

Upvotes: 27

jered
jered

Reputation: 11581

Yep, you can use ref.

Read more about how ref works in the official documentation: https://facebook.github.io/react/docs/refs-and-the-dom.html

Basically, ref is just a callback that is run when a component renders for the first time, immediately before componentDidMount is called. The parameter in the callback is the DOM element that is calling the ref function. So if you have something like this:

var MyComponent = React.createClass({
  render: function(){
    return <div id="my-component" ref={(el) => {this.domElement = el}}>Hello World</div>
  }
})

When MyComponent mounts it will call the ref function that sets this.domElement to the DOM element #my-component.

With that, it's fairly easy to use something like getBoundingClientRect() to measure your DOM elements after they render and determine if the children overflow the parent:

https://jsbin.com/lexonoyamu/edit?js,console,output

Keep in mind there is no way to measure the size/overflow of DOM elements before they render because by definition they don't exist yet. You can't measure the width/height of something until you render it to the screen.

Upvotes: 12

Related Questions