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.
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:
-
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 -
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 -
1st INITIAL rendering completes -> we have a ref to the div now
-
The 2nd (queued) render occurs, firing the
componentDidUpdate
-> we can calculate the overflow and set the flag to true when the content overflows -
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
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…?
Please login or Register to submit your answer