Garmaine Staff asked 1 year ago

I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.

It looks like this: enter image description here

I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:

Class Component

import React, { createRef } from "react"
import styled from "@emotion/styled"

import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"

const StyledHeightContainer = styled.div`
  max-height: 150px;
  overflow: hidden;
`

class ParagraphList extends React.Component {
  state = {
    overflowActive: false,
  }
  wrapper = createRef() // so we can get a ref to the height container

  isOverflowing(el) {
    if (el) return el.offsetHeight < el.scrollHeight
  }

  componentDidMount() {
    this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
  }

  componentDidUpdate() {
    if (this.wrapper.current && !this.state.overflowActive) {
      this.setState({
        overflowActive: this.isOverflowing(this.wrapper.current),
      })
    }
  }

  handleClick() {
    this.setState({ overflowActive: false })
  }

  render() {
    const { moreButtonText, titleText, paragraphs, theme } = this.props

    return (
      <>
        <Section overflowActive={this.state.overflowActive}>
          {this.state.overflowActive || !this.wrapper.current ? (
            <StyledHeightContainer ref={this.wrapper}>
              <Paragraphs paragraphs={paragraphs} />
            </StyledHeightContainer>
          ) : (
            <Paragraphs paragraphs={paragraphs} />
          )}
        </Section>
        {overflowActive ?
         <ButtonReadMore
           onClicked={handleClick.bind(this)}
           moreButtonText={moreButtonText}
           theme={theme}
         />
        : null}
      </>
    )
  }
}

export default ParagraphList

My best way to explain the flow:

  1. When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it

  2. In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass

  3. 1st INITIAL rendering completes -> we have a ref to the div now

  4. The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows

  5. When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.

Functional Component With Hooks

Sandbox of the code

When I re-wrote this as a functional component using Hooks, I ended up with this:

import React, { createRef, useEffect, useState } from "react"
import styled from "@emotion/styled"

import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"

const StyledHeightContainer = styled.div`
  max-height: 150px;
  overflow: hidden;
`

const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
  const [overflowActive, setOverflowActive] = useState(false)
  const [userClicked, setUserClicked] = useState(false)
  const wrapper = createRef(false) // so we can get a ref to the height container

  const isOverflowing = el => {
    if (el) return el.offsetHeight < el.scrollHeight
  }

  useEffect(() => {
    if (!userClicked && !overflowActive && wrapper.current) {
      setOverflowActive(isOverflowing(wrapper.current))
    }
  }, [userClicked]) // note: we only care about state change if user clicks 'Read More' button

  const handleClick = () => {
    setOverflowActive(false)
    setUserClicked(true)
  }

  return (
    <>
      <Section theme={theme} overflowActive={overflowActive}>
        {!userClicked && (overflowActive || !wrapper.current)  ? (
          <StyledHeightContainer ref={wrapper}>
            <Paragraphs paragraphs={paragraphs} />
          </StyledHeightContainer>
        ) : (
          <Paragraphs paragraphs={paragraphs} />
        )}
      </Section>
      {overflowActive ?
        <ButtonReadMore
          onClicked={handleClick.bind(null)}
          moreButtonText={moreButtonText}
          theme={theme}
        />
        : null}
    </>
  )
}

export default ParagraphList

I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).

Is this correct or can someone see a more concise way to write the 2nd solution?

NOTE

One of the reasons I ask is because in the console I get this warning:

48:6  warning  React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array  react-hooks/exhaustive-deps

and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change…?