2
\$\begingroup\$

I'm writing a ctrl+f function so to speak in React that allows a user to search for a term in a list of words, and if the term matches any of the words in the list then it highlights it. To do this, I'm currently using document.querySelectorAll to retrieve all of the words in the list and manipulate them (highlight) if there is a match. Because this is React I feel very bad about using the querySelector to grab the words and feel I should be using refs perhaps, but not sure how comparable that is or if that works in this particular scenario. Here is my code:

const HighlightSearchTerm = () => {
  const [searchTerm, setSearchTerm] = React.useState("");

  React.useEffect(() => {
    const regex = new RegExp(searchTerm, "gi");
    const labels = document.querySelectorAll(".label"); // use refs??

    labels.forEach(label => {
      label.innerHTML = label.innerText.replace(regex, match =>
        `<span class="highlight">${match}</span>`
      );
    });
  }, [searchTerm]);

  return (
    <div>
      <input 
        type="text"
        value={searchTerm}
        placeholder="Search Labels"
        onChange={event => setSearchTerm(event.target.value)}
      />
    </div>
  );
};

As you can see, implementing it this way has to also call document.querySelectorAll each time the user types a letter or makes a change which isn't very efficient and is causing [Violation] 'setTimeout' handler took <N>ms to appear in the console many times when working with large lists. Instead, I would like to

  1. Not use document.querySelectorAll to retrieve the words in the list (use refs if possible?)
  2. Not fetch the list of words each time I type due to the fact the list won't change once the component is rendered (possibly render them when the component mounts, although it seems I can't do this currently when using querySelector)

Any other optimization suggestions would also be very much appreciated.

Current working example: https://codepen.io/andrewgarrison/pen/eYOWjZB

\$\endgroup\$
2
\$\begingroup\$

Rather than extract the Search input in to a different component I would suggest you keep the input and list in the same component.

This way when printing the list you can highlight the search term directly.

const HighlightableList = (props) => {
  const [searchTerm, setSearchTerm] = React.useState("");
  const regex = new RegExp(searchTerm, "gi");

  const items = props.terms.map((term, index) => (
      <div
        className="label"
        key={index}
        dangerouslySetInnerHTML={{
          __html: term.replace(regex, match => `<span class="highlight">${match}</span>`)
        }}
      />
    ));

    return (
      <>
        <input 
          type="text"
          value={searchTerm}
          placeholder="Search Labels"
          onChange={event => setSearchTerm(event.target.value)}
        />
        <div className="label-container">
          {items}
        </div>
      </>
    );
}

https://codepen.io/rockingskier/pen/YzKrGNR

Side notes:

Don't make each list item a label. From MDN

The HTML element represents a caption for an item in a user interface.

That would suggest that each item in the list has its own user input. In fact there is only one input (search) and that should really have a label of its own. I'll let you implement that.

Additionally due the the "list-iness" of the data I have converted it to a ul with each item as an li.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.