67

I'm trying to replace parts of a string with JSX tags, like so:

render: function() {
    result = this.props.text.replace(":",<div className="spacer"></div>);
    return (
         <div>        
             {result}
         <div>        
    );
}

But given that this.props.text is Lorem : ipsum, it results in

<div>
    Lorem [object Object] ipsum.
</div>

Is there a way to solve this or another way to replace parts of a string with JSX tags?

Dmitry Shvedov
  • 3,169
  • 4
  • 39
  • 51
Magnus Engdal
  • 5,446
  • 3
  • 31
  • 50

18 Answers18

53

The accepted answer worked well in 2015, but now better solutions exist.

For this problem issue #3368 was created and based on the solution provided by a Facebook employee working on React, react-string-replace was created.

Using react-string-replace, here is how you can solve your problem

const reactStringReplace = require('react-string-replace');

const HighlightNumbers = React.createClass({
  render() {
    const content = 'Hey my number is 555:555:5555.';
    return (
      <span>
        {reactStringReplace(content, ':', (match, i) => (
          <div className="spacer"></div>
        ))}
      </span>
    );
  },
});
Matthew Barbara
  • 3,792
  • 2
  • 21
  • 32
35

When you pass a JSX element to replace() as the second argument, that element is converted to a string because replace() expects a string as a second argument. What you need to do is convert your string to an array of strings and JSX elements. So your result variable should contain something like ['Lorem ', <div className="spacer"></div>, ' ipsum'].

Something like this:

function flatMap(array, fn) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    var mapping = fn(array[i]);
    result = result.concat(mapping);
  }
  return result;
}

var Comp = React.createClass({
  render: function () {
    var result = 'Lorem : ipsum';
    result = flatMap(result.split(':'), function (part) {
      return [part, <div>spacer</div>];
    });
    // Remove the last spacer
    result.pop();
    return (
      <div>        
        {result}
      </div>
    );
  }
});
Anders Ekdahl
  • 22,685
  • 4
  • 70
  • 59
  • Anders, this is great! Do you know how one would go around doing the same but for having (and rendering) static HTML inside the string? If Lorem is wrappend in a DIV, rendering { result } inside dangerouslySetInnerHTML gives all sorts of (expected) trouble. – Francisco Aquino Jun 09 '15 at 17:36
  • 1
    @MagnusEngdal The accepted answer is two years old. I encountered this problem today and based on the research that I did today, I believe my answer is the best solution for such problem. – Matthew Barbara Oct 19 '17 at 11:43
  • React.createClass has been deprecated since v 15.5.0, any alternative solution to this? – try_catch Mar 12 '22 at 17:23
12

Wrote a utility function for jsx.

const wrapTags = (text: string, regex: RegExp, className?: string) => {
  const textArray = text.split(regex);
  return textArray.map(str => {
    if (regex.test(str)) {
      return <span className={className}>{str}</span>;
    }
    return str;
  });
};

Tushar Tilwani
  • 121
  • 1
  • 4
  • 4
    This is gold! However, it is best practice to tell what your script actually do and how t. :) – holm50 Dec 19 '19 at 12:38
11

As I am a perfectionist. I think this pattern is perfect:

String.prototype.replaceJSX = function (find, replace) {
    return this.split(find).flatMap((item) => [item, replace]).slice(0, -1);
}

Usage:

variable.replaceJSX(":", <br />);
Amir Fo
  • 5,163
  • 1
  • 43
  • 51
  • 2
    This doesn't works, it repeats the replace in the end of the array, and introduce some weird empty strings. also its not a good practice overriding String.prototype – Elias Soares Mar 10 '23 at 19:21
  • 1
    Not sure about the empty strings, but ```find.split(replace).flatMap((i)=>[i,replace]).slice(0,-1)``` is working for me – SkyNT Mar 17 '23 at 01:46
9

The following should also work (assumes ES6), The only nuance is that the text is actually wrapped inside a DIV element and not preceded by it, assuming you are going to use CSS for the actual spacing this shouldn't be a problem.

const result = this.props.text.split(':').map(t => {
  return <div className='textItem'>{t}</div>;
});
GalAbra
  • 5,048
  • 4
  • 23
  • 42
Andy Polhill
  • 6,858
  • 2
  • 24
  • 20
5

I have come to following simple solution that does not include third party library or regex, maybe it can still help someone.

Mainly just use .replace() to replace string with regular html written as string, like:

text.replace('string-to-replace', '<span class="something"></span>')

And then render it using dangerouslySetInnerHTML inside an element.

Full example:

const textToRepace = 'Insert :' // we will replace : with div spacer
const content = textToRepace.replace(':', '<div class="spacer"></div>') : ''

// then in rendering just use dangerouslySetInnerHTML
render() {
    return(
        <div dangerouslySetInnerHTML={{
            __html: content
        }} />
    )
}
GalAbra
  • 5,048
  • 4
  • 23
  • 42
Aleksej
  • 469
  • 3
  • 7
3

I just a wrote function helper to replace some Texts, Components, and even HTMLs to a template string for my project. It turned out so nice and smooth.

const replaceJSX = (str, replacement) => {
    const result = [];
    const keys = Object.keys(replacement);
    const getRegExp = () => {
        const regexp = [];
        keys.forEach((key) => regexp.push(`{${key}}`));
        return new RegExp(regexp.join('|'));
    };
    str.split(getRegExp()).forEach((item, i) => {
        result.push(item, replacement[keys[i]]);
    });
    return result;
};

Usage:

const User = ({ href, name }) => {
    return (
        <a href={href} title={name}>
            {name}
        </a>
    );
};

const string = 'Hello {component}, {html}, {string}';

return (
    <div>
        {replaceJSX(string, {
            component: (
                <User
                    href='https://stackoverflow.com/users/64730/magnus-engdal'
                    name='Magnus Engdal'
                />
            ),
            html: (
                <span style={{ fontWeight: 'bold' }}>
                    This would be your solution
                </span>
            ),
            string: 'Enjoy!',
        })}
    </div>
)

And you'll get something like this:

<div>Hello <a href="https://stackoverflow.com/users/64730/magnus-engdal" title="Magnus Engdal">Magnus Engdal</a>, <span style="font-weight: bold;">This would be your solution.</span>, Enjoy!.</div>
Spleen
  • 2,656
  • 1
  • 10
  • 16
2

After some research I found that existing libraries doesn't fit my requirements. So, of course, I have written my own:

https://github.com/EfogDev/react-process-string

It is very easy to use. Your case example:

let result = processString({
    regex: /:/gim,
    fn: () => <div className="spacer"></div>
})(this.props.test);
Efog
  • 1,155
  • 1
  • 15
  • 33
1

I had the more common task - wrap all (English) words by custom tag. My solution:

class WrapWords extends React.Component {
  render() {
    const text = this.props.text;
    const isEnglishWord = /\b([-'a-z]+)\b/ig;
    const CustomWordTag = 'word';

    const byWords = text.split(isEnglishWord);

    return (
    <div>
      {
        byWords.map(word => {
          if (word.match(isEnglishWord)) {
            return <CustomWordTag>{word}</CustomWordTag>;
          }
          return word;
        })
      }
    </div>
    );
    
  }
}

// Render it
ReactDOM.render(
  <WrapWords text="Argentina, were playing: England in the quarter-finals (the 1986 World Cup in Mexico). In the 52nd minute the Argentinian captain, Diego Maradona, scored a goal." />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="react"></div>
Londeren
  • 3,202
  • 25
  • 26
1

Nothing around WEB globe worked for me exept this solution - https://www.npmjs.com/package/regexify-string

Working with React, strings and doesn't have any additional dependecies

regexifyString({
    pattern: /\[.*?\]/gim,
    decorator: (match, index) => {
        return (
            <Link
                to={SOME_ROUTE}
                onClick={onClick}
            >
                {match}
            </Link>
        );
    },
    input: 'Some text with [link]',
});
Kas Elvirov
  • 7,394
  • 4
  • 40
  • 62
1

Something like this:

function replaceByComponent(string, component) {
  const variable = string.substring(
    string.lastIndexOf("{{") + 2, 
    string.lastIndexOf("}}")
  );
  const strParts = string.split(`{{${variable}}}`);
  const strComponent = strParts.map((strPart, index) => {
    if(index === strParts.length - 1) {
      return strPart
    }
    return (   
      <>
        {strPart}
        <span>
          {component}
        </span>
      </>
    )
  })
  return strComponent
}
arnaudjnn
  • 923
  • 10
  • 12
1

In my case, I use React and I wanted to replace url in text with anchor tag.

In my solution, I used two library.

and wrote this code.

/* eslint-disable react/no-danger */
import React from 'react';
import { Parser } from 'simple-text-parser';
import urlRegex from 'url-regex';

type TextRendererProps = { text: string };

const parser = new Parser();
const re = urlRegex();

parser.addRule(re, (url) => {
  return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});

export const TextRenderer: React.FC<TextRendererProps> = ({ text }) => {
  return (
    <span
      dangerouslySetInnerHTML={{
        __html: parser.render(text),
      }}
    />
  );
};

You can easily add replacing rule by just writing parser.addRule().

kelsny
  • 23,009
  • 3
  • 19
  • 48
mozu
  • 729
  • 1
  • 7
  • 10
1

Adding recursiveness to @Amir Fo's answer:

export const replaceJSX = (subject, find, replace) => {
    const result = [];
    if(Array.isArray(subject)){
      for(let part of subject)
        result = [ ...result, replaceJSX(part, find, replace) ]
      return result;
    }else if(typeof subject !== 'string')
      return subject;
    let parts = subject.split(find);
    for(let i = 0; i < parts.length; i++) {
        result.push(parts[i]);
        if((i+1) !== parts.length)
          result.push(replace);
    }
    return result;
}

export const replaceJSXRecursive = (subject, replacements) => {
  for(let key in replacements){
    subject = replaceJSX(subject, key, replacements[key])
  }
  return subject;
}

You can now replace any number of strings with JSX elements at once by calling replaceJSXRecursive like below:

replaceJSXRecursive(textVar, {
  ':': <div class="spacer"></div>,
  ';': <div  class="spacer2"></div>
})
Mahesh
  • 603
  • 1
  • 10
  • 22
0

If you'd also like to be able to make replacements within replacements (for example, to highlight search terms within urls), check out this node module I created - https://github.com/marcellosachs/react-string-replace-recursively

0

Example with hooks:

import React, { useEffect, useState, useRef } from 'react'

export function Highlight({ value, highlightText }) {
  const [result, resultSet] = useState(wrap())

  const isFirstRun = useRef(true) 

  function wrap() {
    let reg = new RegExp('(' + highlightText + ')', 'gi')
    let parts = value.split(reg)

    for (let i = 1; i < parts.length; i += 2) {
      parts[i] = (
        <span className='highlight' key={i}>
          {parts[i]}
        </span>
      )
    }
    return <div>{parts}</div>
  }

  useEffect(() => {
    //skip first run
    if (isFirstRun.current) {
      isFirstRun.current = false
      return
    }
    resultSet(wrap())
  }, [value, highlightText])

  return result
}
ZiiMakc
  • 31,187
  • 24
  • 65
  • 105
0

i think this is the most light perfect solution:

render() {
    const searchValue = "an";
    const searchWordRegex = new RegExp(searchValue, "gi");
    const text =
      "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text";
    return (
      <div>
        {text.split(searchWordRegex).length > 1
          ? text.split(searchWordRegex).map((chunk, index) => {
              if (chunk !== "") {
                return index === 0 &&
                  ! new RegExp("^" + searchValue, "gi").test(text) ? (
                  chunk
                ) : (
                  <span key={index}>
                    <span
                      className="highlight"
                      style={{
                        fontWeight: "bold"
                      }}
                    >
                      {searchValue.charAt(0).toUpperCase() +
                        searchValue.slice(1)}
                    </span>
                    {chunk}
                  </span>
                );
              } else {
                return null;
              }
            })
          : text}
      </div>
    );
  }

and here is a working example

Biskrem Muhammad
  • 4,074
  • 3
  • 31
  • 38
0

1.use regex capture the words you want 2.str.spilt(regex)

eg:

var string4 = 'one split two splat three splot four';
var splitString4 = string4.split(/\ (split|splat|splot)\ /);
console.log(splitString4); // Outputs ["one", "split", "two", "splat", "three", "splot", "four"]

3.render array

arr.map(i=> <div>{i}</div>) 


 
0

This is mostly similar to Spleen's answer, however I'm unable to edit it (edit queue full) so posting it as a separate answer.

The key differences with the below is that it's been structured towards Typescript and more importantly it won't embed the replacement value at the end of the string if it cannot be found.

export const replaceWithJsx = (
  valueToUpdate: string,
  replacement: Record<string, JSX.Element>,
) => {
  const result: Array<string | JSX.Element> = [];

  const keys = Object.keys(replacement);
  const regExp = new RegExp(keys.join("|"));

  valueToUpdate.split(regExp).forEach((item, index) => {
    result.push(item);

    const key = keys[index];
    if (valueToUpdate.includes(key)) {
      result.push(replacement[key]);
    }
  });

  return result;
};
Damian
  • 574
  • 1
  • 6
  • 19