'Highlight text using ReactJS

I'm trying to highlight text matching the query but I can't figure out how to get the tags to display as HTML instead of text.

var Component = React.createClass({
    _highlightQuery: function(name, query) {
        var regex = new RegExp("(" + query + ")", "gi");
        return name.replace(regex, "<strong>$1</strong>");
    },
    render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" /> {this._highlightQuery(name, query)}
            </div>
        );
    }
});

Current Output: <strong>Java</strong>script

Desired Output: Javascript



Solution 1:[1]

Here is an example of a react component that uses the standard <mark> tag to highlight a text:

const Highlighted = ({text = '', highlight = ''}) => {
   if (!highlight.trim()) {
     return <span>{text}</span>
   }
   const regex = new RegExp(`(${_.escapeRegExp(highlight)})`, 'gi')
   const parts = text.split(regex)
   return (
     <span>
        {parts.filter(part => part).map((part, i) => (
            regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
        ))}
    </span>
   )
}

And here is how to use it

<Highlighted text="the quick brown fox jumps over the lazy dog" highlight="fox"/>

Solution 2:[2]

There is already a react component on NPM to do what you want:

var Highlight = require('react-highlighter');
[...]
<Highlight search={regex}>{name}</Highlight>

Solution 3:[3]

Here's my solution.

I tried to focus on simplicity and performance, so I avoided solutions that involved manual manipulation of the DOM outside of React, or unsafe methods like dangerouslySetInnerHTML.

Additionally, this solution takes care of combining subsequent matches into a single <span/>, thus avoiding having redundant spans.

const Highlighter = ({children, highlight}) => {
  if (!highlight) return children;
  const regexp = new RegExp(highlight, 'g');
  const matches = children.match(regexp);
  console.log(matches, parts);
  var parts = children.split(new RegExp(`${highlight.replace()}`, 'g'));

  for (var i = 0; i < parts.length; i++) {
    if (i !== parts.length - 1) {
      let match = matches[i];
      // While the next part is an empty string, merge the corresponding match with the current
      // match into a single <span/> to avoid consequent spans with nothing between them.
      while(parts[i + 1] === '') {
        match += matches[++i];
      }

      parts[i] = (
        <React.Fragment key={i}>
          {parts[i]}<span className="highlighted">{match}</span>
        </React.Fragment>
      );
    }
  }
  return <div className="highlighter">{parts}</div>;
};

Usage:

<Highlighter highlight='text'>Some text to be highlighted</Highlighter>

Check out this codepen for a live example.

Solution 4:[4]

By default ReactJS escapes HTML to prevent XSS. If you do wish to set HTML you need to use the special attribute dangerouslySetInnerHTML. Try the following code:

render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" /> <span dangerouslySetInnerHTML={{__html: this._highlightQuery(name, query)}}></span>
            </div>
        );
    }

Solution 5:[5]

  const escapeRegExp = (str = '') => (
    str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
  );

  const Highlight = ({ search = '', children = '' }) => {
    const patt = new RegExp(`(${escapeRegExp(search)})`, 'i');
    const parts = String(children).split(patt);

    if (search) {
      return parts.map((part, index) => (
        patt.test(part) ? <mark key={index}>{part}</mark> : part
      ));
    } else {
      return children;
    }
  };

  <Highlight search="la">La La Land</Highlight>

Solution 6:[6]

Mark matches as a function https://codesandbox.io/s/pensive-diffie-nwwxe?file=/src/App.js

import React from "react";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      res: "Lorem ipsum dolor"
    };
    this.markMatches = this.markMatches.bind(this);
  }
  markMatches(ev) {
    let res = "Lorem ipsum dolor";
    const req = ev.target.value;
    if (req) {
      const normReq = req
        .toLowerCase()
        .replace(/\s+/g, " ")
        .trim()
        .split(" ")
        .sort((a, b) => b.length - a.length);
      res = res.replace(
        new RegExp(`(${normReq.join("|")})`, "gi"),
        match => "<mark>" + match + "</mark>"
      );
    }
    this.setState({
      res: res
    });
  }

  render() {
    return (
      <div className="App">
        <input type="text" onChange={this.markMatches} />
        <br />
        <p dangerouslySetInnerHTML={{ __html: this.state.res }} />
      </div>
    );
  }
}

export default App;

Solution 7:[7]

This should work:

var Component = React.createClass({
    _highlightQuery: function(name, query) {
        var regex = new RegExp("(" + query + ")", "gi");
        return "<span>"+name.replace(regex, "<strong>$1</strong>")+"</span>";
    },
    render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" />{JSXTransformer.exec(this._highlightQuery(name, query))}
            </div>
        );
    }
});

Basically you're generating a react component on the fly. If you want, you can put the <span> tag inside the render() function rather then the _highlightQuery() one.

Solution 8:[8]

I would suggest you use a different approach. Create one component, say <TextContainer />, which contains <Text /> elements.

var React = require('react');
var Text = require('Text.jsx');

var TextContainer = React.createClass({
    getInitialState: function() {
        return {
            query: ''
        };
    },
    render: function() {
        var names = this.props.names.map(function (name) {
            return <Text name={name} query={this.state.query} />
        });
        return (
            <div>
                {names}
           </div>
        );
    }
});

module.exports = TextContainer;

As you see the text container holds as state the current query. Now, the <Text /> component could be something like this:

var React = require('react');

var Text = React.createClass({
    propTypes: {
        name: React.PropTypes.string.isRequired,
        query: React.PropTypes.string.isRequired
    },

    render: function() {
        var query = this.props.query;
        var regex = new RegExp("(" + query + ")", "gi");
        var name = this.props.name;
        var parts = name.split(regex);
        var result = name;

        if (parts) {
            if (parts.length === 2) {
                result =
                    <span>{parts[0]}<strong>{query}</strong>{parts[1]}</span>;
            } else {
                if (name.search(regex) === 0) {
                    result = <span><strong>{query}</strong>{parts[0]}</span>
                } else {
                    result = <span>{query}<strong>{parts[0]}</strong></span>
                }
            }
        }

        return <span>{result}</span>;
    }

});

module.exports = Text;

So, the root component has as state, the current query. When its state will be changed, it will trigger the children's render() method. Each child will receive the new query as a new prop, and output the text, highlighting those parts that would match the query.

Solution 9:[9]

I had the requirement to search among the comments contain the HTML tags.

eg: One of my comments looks like below example

Hello World

<div>Hello<strong>World</strong></div>

So, I wanted to search among all these kinds of comments and highlight the search result.

As we all know we can highlight text using HTML tag <mark>

So. I have created one helper function which performs the task of adding <mark> tag in the text if it contains the searched text.

getHighlightedText = (text, highlight) => {
    if (!highlight.trim()) {
      return text;
    }
    const regex = new RegExp(`(${highlight})`, "gi");
    const parts = text.split(regex);
    const updatedParts = parts
      .filter((part) => part)
      .map((part, i) =>
        regex.test(part) ? <mark key={i}>{part}</mark> : part
      );
    let newText = "";
    [...updatedParts].map(
      (parts) =>
        (newText =
          newText +
          (typeof parts === "object"
            ? `<${parts["type"]}>${highlight}</${parts["type"]}>`
            : parts))
    );
    return newText;
  };

So, We have to pass our text and search text inside the function as arguments.

Input

getHighlightedText("<div>Hello<strong>World</strong></div>", "hello")

Output

<div><mark>Hello</mark><strong>World</strong></div>

Let me know if need more help with solutions.

Solution 10:[10]

Based on @Henok T's solution, here is one without lodash.

It is implement in Typescript and uses Styled-components, but can be easily adapted to vanilla JS, by simply removing the types and adding the styles inline.

import React, { useMemo } from "react";
import styled from "styled-components";

const MarkedText = styled.mark`
  background-color: #ffd580;
`;

interface IHighlighted { 
  text?: string;
  search?: string;
}

export default function Highlighted({ text = "", search = "" }: IHighlighted): JSX.Element {
  /**
   * The brackets around the re variable keeps it in the array when splitting and does not affect testing
   * @example 'react'.split(/(ac)/gi) => ['re', 'ac', 't']
   */
  const re = useMemo(() => {
    const SPECIAL_CHAR_RE = /([.?*+^$[\]\\(){}|-])/g;
    const escapedSearch = search.replace(SPECIAL_CHAR_RE, "\\$1");
    return new RegExp(`(${escapedSearch})`, "i");
  }, [search]);

  return (
    <span>
      {search === ""
        ? text
        : text
            .split(re)
            .filter((part) => part !== "")
            .map((part, i) => (re.test(part) ? <MarkedText key={part + i}>{part}</MarkedText> : part))}
    </span>
  );
}

Solution 11:[11]

With react-mark.js you can simply:

<Marker mark="hello">
  Hello World
</Marker>

Links:

Solution 12:[12]

I have extended the version from @Henok T from above to be able to highlight multiple text parts splitted by space but keep strings in quotes or double quotes together.

e.g. a highlight of text "some text" 'some other text' text2 would highlight the texts:

text some text some other text text2 in the given text.

 const Highlighted = ({text = '', highlight = ''}: { text: string; highlight: string; }) => {
    if (!highlight.trim()) {
        return <span>{text}</span>
    }
  
    var highlightRegex = /'([^']*)'|"([^"]*)"|(\S+)/gi;  // search for all strings but keep strings with "" or '' together
    var highlightArray = (highlight.match(highlightRegex) || []).map(m => m.replace(highlightRegex, '$1$2$3'));

    // join the escaped parts with | to a string
    const regexpPart= highlightArray.map((a) => `${_.escapeRegExp(a)}`).join('|');
    
    // add the regular expression
    const regex = new RegExp(`(${regexpPart})`, 'gi')
   
    const parts = text.split(regex)
    return (
        <span>
            {parts.filter(part => part).map((part, i) => (
                regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
            ))}
        </span>
    )
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Steve
Solution 2 aij
Solution 3 Yoav Kadosh
Solution 4 Yanik Ceulemans
Solution 5
Solution 6
Solution 7
Solution 8 Todd
Solution 9 Krunal Rajkotiya
Solution 10
Solution 11 Dharman
Solution 12 BHoft