I'm trying to make a reusable React text-clamp component. The user passes in the number of lines to render and the text they want to display, and the component renders their text, cutting it off at the specified number of lines and inserting an ellipsis (...) at the end.
The way I'm calculating where to cut off the text and insert the ellipsis is to add one word at a time until the clientHeight of the text is bigger than the clientHeight of the container div.
While it works, I'm seeing the following in the chrome dev tools:
[Violation] Forced reflow while executing JavaScript took 179ms.
This is probably due to the fact that reading clientHeight forces reflow.
Here's my code:
class TextClamp extends React.PureComponent {
constructor(props) {
super(props);
this.renderText = this.renderText.bind(this);
this.state = {
words: this.props.textToDisplay.split(' '),
};
}
componentDidMount() {
this.renderText();
}
renderText(isResizing = false) {
const textEl = this.displayedText;
const clampContainer = this.clampContainer;
const heightToStop = isResizing ? clampContainer.style.height : this.letterHeightText.clientHeight * this.props.linesToRender;
const dummyText = this.dummyText;
const dummyDiv = this.dummyDiv;
const words = this.state.words;
const numWords = words.length;
dummyDiv.style.cssText = `width: ${clampContainer.clientWidth}px; position: absolute; left: -1000px;`;
let i = this.props.estimatedWordCount || 20;
let strToRender = words.slice(0, i).join(' ');
dummyText.textContent = strToRender;
if (dummyText.clientHeight <= heightToStop && i>=numWords) {
return;
}
while (dummyText.clientHeight <= heightToStop && i<numWords) {
dummyText.textContent += ' ' + words[i++];
};
strToRender = dummyText.textContent;
while (dummyText.clientHeight > heightToStop) {
strToRender = strToRender.substring(0, strToRender.lastIndexOf(' '));
dummyText.textContent = strToRender + '\u2026';
}
textEl.textContent = dummyText.textContent;
}
render() {
const estimatedHeight = this.props.estimatedHeight || 20 * this.props.linesToRender;
const containerStyle = { height: estimatedHeight, overflow: 'hidden'};
if (typeof window !== 'undefined') {
const dummyDiv = document.createElement('div');
const dummyText = document.createElement('p');
dummyDiv.appendChild(dummyText);
this.dummyDiv = dummyDiv
this.dummyText = dummyText
document.body.appendChild(dummyDiv);
}
return (
<div style={containerStyle} ref={(input) => {this.clampContainer = input;}}>
<p ref={(input) => {this.displayedText = input;}}>{this.props.textToDisplay}</p>
<p style={{visibility: 'hidden'}} ref={(input) => {this.letterHeightText = input;}}>Q</p>
</div>
);
}
}
So basically, the main workhorse of the component is the renderText() function. In there I'm adding one word at a time until the height of the text is greater than that of its container. From there, I remove the last word and add the ellipsis.
The optimizations I've made are the following:
estimatedWordCount allows the loop that adds one word a time to not have to start at the beginning each time.
I calculate the text that should be displayed by copying the dimensions of the actual container div to an offscreen,
position:absolutediv so it has no interaction with the other DOM elements.
However, even with my optimizations chrome is still complaining that reflow due to javascript is taking too long.
Are there any optimizations to my renderText() function I can make to avoid reading the clientHeight so often?