22

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?

Community
  • 1
  • 1
Jemar Jones
  • 1,491
  • 2
  • 21
  • 27

6 Answers6

25

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;
},
Jemar Jones
  • 1,491
  • 2
  • 21
  • 27
15

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

Rinor
  • 1,790
  • 13
  • 22
  • 3
    Thanks for the working code example. I found I also needed to run isEllipsisActive from componentDidUpdate, and in order to avoid infinite looping I needed to add a check before updating state: if (this.isEllipsisActive(this.span) !== this.state.overflowActive) { // update state here } – Little Brain Feb 11 '19 at 17:57
12

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.

jered
  • 11,220
  • 2
  • 23
  • 34
  • Could you possibly show how something like what you showed in your jsbin with `getBoundingClientRect` would work work when the element has multiple children? – Jemar Jones Feb 02 '17 at 21:36
  • A `div` will "wrap" everything inside of it as long as they are all positioned relatively or statically. So if you put all of the `children` inside of a `div` inside of the parent (like it's structured in the jsbin) then it should work out. – jered Feb 02 '17 at 21:44
  • If you have a more complicated use case then you should provide more detail and some examples of your code, ideally in a separate question. – jered Feb 02 '17 at 21:45
  • Alright i actually just realized something else. In your jsbin the type of the __reactInternalInstance coming back from the refs is `ReactDOMComponent`, whereas in my similar code gives me `ReactCompositeComponentWrapper`. And this instance that i'm getting back doesn't have the same properties as yours for some reason. Would you happen to understand why? – Jemar Jones Feb 02 '17 at 22:01
  • I realized that i can actually just wrap my Component in a div in my situation, and then i am able to access the normal dom element properties! Thanks! – Jemar Jones Feb 03 '17 at 14:17
10

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',
});
Kraw24
  • 143
  • 1
  • 7
6

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>
)}
athi
  • 1,683
  • 15
  • 26
afalak
  • 481
  • 5
  • 9
4

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