0

I am trying to get the height on an element.

I want the parent div to adjust its height to the first paragraph of its children paragraph elements. Then a "Read More"/"Read Less" button expands the parent div to reveal all paragraphs or shrinks to only one paragraph.

I have experimented with useEffect, useLayoutEffect and componentDidMount in a class component and they all seem to need a setTimeout delay for the parent div to attain the perfect height.

Sorry if my code is fuzzy. I am new to React. :)

TIA

import styled from 'styled-components'
import React, { useRef, useLayoutEffect, useState } from 'react'

const CatIntroStyled = styled.div`
    width: 1000px;
    margin: 0 auto;

    button{
        display: block;
        margin: 0 auto;
        background: none;
    }

    button:focus{
        outline: none;
    }
`

const IntroText = styled.div`
    height: ${({introStyle})=>{
        if(introStyle.initIntroHeight === "auto") return "auto"
        return introStyle.introExpanded ? introStyle.initIntroHeight+"px": introStyle.initFirstPara+"px";
    }};
    overflow: hidden;
    transition: all 1s;
    margin-bottom: 2rem
`

const formatIntro = (text, paraRef)=>{

    let formatedText = text.replace(/<[^>]*>?/gm, "").replace(/\n\r/g, "")
    let returnText = formatedText.split("\r\n").map((paragraph, key) => {
        if(key===0) return <p ref={paraRef} key={key}>{paragraph}</p>;
        return <p key={key}>{paragraph}</p>
    })
    return returnText

}

const CatIntro =  ({title, text})=>{
    
    const firstIntroPara = useRef();
    const introRef = useRef();

    const [intro, setIntro] = useState({
        initFirstPara: 0,
        initIntroHeight: "auto",
        introExpanded: false
    })

    useLayoutEffect(()=>{
        setTimeout(()=>{
            setIntro({
                ...intro,
                initIntroHeight: introRef.current.offsetHeight,
                initFirstPara: firstIntroPara.current.offsetHeight,
            })
        }, 1000)

    }, [])

    return(
        <CatIntroStyled>
            <h1 className="globalTitleStyle">{title}</h1>
            <IntroText ref={introRef} introStyle={intro}>
                {formatIntro(text, firstIntroPara)}
            </IntroText>
            <button onClick={(e)=>{
                setIntro({
                    ...intro,
                    introExpanded: !intro.introExpanded
                })
            }}>{ intro.introExpanded ? "READ LESS": "READ MORE" }</button>
        </CatIntroStyled>
    )

}


export default CatIntro

Is there a more robust way of knowing when elements are truly painted on the screen?

TIA

CalderW
  • 1
  • 1

2 Answers2

1

Try using useLayoutEffect.

This runs synchronously immediately after React has performed all DOM mutations. This can be useful if you need to make DOM measurements (like getting the scroll position or other styles for an element).

Example

function App() {
  const divRef = React.useRef(null);

  React.useLayoutEffect(() => {
    console.log(divRef.current.clientHeight)
  }, [])

  return (
    <div ref={divRef} style={{ height: 100, width: 100, backgroundColor: 'red' }}/>
  );
}

For this :- You need to change your class component to functional ones.

Prateek Thapa
  • 4,829
  • 1
  • 9
  • 23
  • Thanks for your reply. I had experimented with useLayoutEffect initially. I have updated my code to reflect this. It also seems to need a setTimeout for it to work properly. – CalderW Sep 12 '20 at 05:49
  • Could you create a sanbox here :- https://stackblitz.com/edit/react-jhtwhb – Prateek Thapa Sep 12 '20 at 05:53
0

Maybe i don't fully understand why you need the height.

But if each child of the component is a paragraph, and you either want to show all the paragraphs when expanded, but only one paragraph when not expanded, you could do something like this:

import React, { useState } from "react";

export default function Expandable({ children, initial = false }) {
  const [expanded, setExpanded] = useState(initial);

  return (
    <div>
      {expanded ? children : [...children].slice(0,1)}
      <button onClick={() => setExpanded(!expanded)}>{`Read ${
        expanded ? "less" : "more"
      }`}</button>
    </div>
  );
}

Then you could consume the component like this:

  <Expandable>
    <p>
      egestas ultrices. Curabitur eget lorem eu augue pretium blandit at non
      metus. Mauris a venenatis tellus, vel mollis leo. Vivamus nec
      elementum neque, non mollis felis.
    </p>
    <p>
      fringilla. Sed convallis sem sed diam vehicula egestas. In tincidunt
      hendrerit elit, eu facilisis leo vulputate id. Sed rutrum imperdiet
      convallis. Nam mi magna, lacinia vitae consequat vel, consequat eget
      ex. Maecenas nec ex egestas, mattis orci sit amet, dictum sem. Sed id
      tincidunt felis. Vivamus ipsum erat, sagittis sed consequat et,
      molestie a risus. Quisque nec risus fringilla, pellentesque leo a,
      venenatis leo.
    </p>
    <p>
      est in varius pulvinar. Ut dignissim condimentum semper. Vestibulum
      blandit purus vitae dapibus finibus. Nam iaculis metus orci, et
      posuere lectus imperdiet at. Suspendisse non erat tortor.
    </p>
    <p>ullamcorper sagittis.</p>
  </Expandable>

Edit lucid-hamilton-mo450

Edit:

You can get the height of the first paragraph like this.

Note: with this approach, you probably need to listen for a resize event and adjust the value of the height state.

import React, { useState, useEffect, useRef } from "react";

export default function Expandable({ children, initial = false }) {
  const [expanded, setExpanded] = useState(initial);
  const [firstParagraphHeight, setFirstParagraphHeight] = useState(0);

  const ref = useRef(null);

  useEffect(() => {
    const height = ref.current.children[0].getBoundingClientRect().height;
    setFirstParagraphHeight(height);
  }, []);

  return (
    <>
      <div
        ref={ref}
        style={{
          overflow: "hidden",
          maxHeight: expanded ? "none" : `${firstParagraphHeight}px`
        }}
      >
        {children}
      </div>
      <button onClick={() => setExpanded(!expanded)}>{`Read ${
        expanded ? "less" : "more"
      }`}</button>
    </>
  );
}

Edit ecstatic-ives-25xe9

ksav
  • 20,015
  • 6
  • 46
  • 66
  • But initially you would get only one paragraph, which impacts SEO. And there isn't the slow reveal of the parent containers transition, just an all or nothing jarring effect. – CalderW Sep 12 '20 at 06:27