React: How to replace parts of a string with a component?

Created on 12 Mar 2015  ·  40Comments  ·  Source: facebook/react

I have this code which is obviously not working:

    _.each(matches, (match) ->
      string = string.replace(match, ->
        <span className="match" key={i++}>{match}</span>
      )
    )

Because that would result into a string mixed with objects. Bad I know. But how can I add React components inside a string? All I want is to highlight parts of a string with a react component. A tough case to crack I guess.

Most helpful comment

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
  parts[i] = <span className="match" key={i}>{parts[i]}</span>;
}
return <div>{parts}</div>;

Let me know if that's unclear.

All 40 comments

something like this should work :

const escapeRE = new RegExp(/([.*+?^=!:$(){}|[\]\/\\])/g)
const safeRE = (string) => {
  return string.replace(escapeRE, "\\$1")
}

class Hightlight extends Component {
  static propTypes = {
    match : PropTypes.string,
    string : PropTypes.string,
  }

  render() {
    return (
      <span
        dangerouslySetInnerHTML={{
          __html : string.replace(safeRE(this.props.match), "<strong className\"match\">$1</strong>")
        }} />
    )
  }
}

Yes, I've been thinking of abusing dangerouslySetInnerHTML but I do not like this idea. This method labelled as dangerously for good reasons.

Is there no way to teach React to parse jsx within a string? Have React parse i.E.

var example = "this one word <span className="match" key={i++}>hello</span> is highlighted"

and return this in a render method?

@binarykitchen You can construct React elements dynamically and that's what you need to do here, you just can't do it with only string replacement.

@syranide Yes, I already know that. My point is: wouldn't this be a nice JSX/React feature? Parse a string and turn any tags inside into child React components.

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
  parts[i] = <span className="match" key={i}>{parts[i]}</span>;
}
return <div>{parts}</div>;

Let me know if that's unclear.

Parsing JSX at runtime is error-prone and slow, and can easily have security implications – it wouldn't inherently be any safer than dangerouslySetInnerHTML is.

that can be a bit of an issue with type case

— mlb

On 12 mars 2015, at 21:37, Ben Alpert [email protected] wrote:

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
parts[i] = ;
}
return

{parts}
;
Let me know if that's unclear.


Reply to this email directly or view it on GitHub.

I ran into this issue as well when trying to highlight parts of a string programatically (i.e. replace with <span> tags). I ended up creating a module based on @spicyj's solution above. For anyone interested: iansinnott/react-string-replace

@iansinnott Thanks for sharing that. I started using it then realized I'm going to need the full String.prototype.replace API (needed access to the individual matching groups for a regexp within the replace function). I realized the change will be more than a simple patch (and require an API change) so I created a new repo: https://github.com/oztune/string-replace-to-array

If react-string-replace doesn't fit your requirements:

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

The following worked for me as way to highlight keyword(s) within a string of text, without
dangerously setting HTML:

/**
 * Find and highlight relevant keywords within a block of text
 * @param  {string} label - The text to parse
 * @param  {string} value - The search keyword to highlight
 * @return {object} A JSX object containing an array of alternating strings and JSX
 */
const formatLabel = (label, value) => {
  if (!value) {
    return label;
  }
  return (<span>
    { label.split(value)
      .reduce((prev, current, i) => {
        if (!i) {
          return [current];
        }
        return prev.concat(<b key={value + current}>{ value }</b>, current);
      }, [])
    }
  </span>);
};

formatLabel('Lorem ipsum dolor sit amet', 'dolor');
// <span>Lorem ipsum <b>dolor</b> sit amet</span>

I needed something like this for mentions matching some pattern so it can wrap multiple values and did some changes to the @richardwestenra solution. Maybe it will be useful for someone.

/**
 * Find and highlight mention given a matching pattern within a block of text
 * @param {string} text - The text to parse
 * @param {array} values - Values to highlight
 * @param {RegExp} regex - The search pattern to highlight 
 * @return {object} A JSX object containing an array of alternating strings and JSX
 */
const formatMentionText = (text, values, regex) => { 
    if (!values.length)
        return text;

    return (<div>
        {text.split(regex)
            .reduce((prev, current, i) => {
                if (!i)
                    return [current];

                return prev.concat(
                    values.includes(current)  ?
                        <mark key={i + current}>
                            {current}
                        </mark>
                        : current
                );
            }, [])}
    </div>);
};

const text = 'Lorem ipsum dolor sit amet [[Jonh Doe]] and [[Jane Doe]]';
const values = ['Jonh Doe', 'Jane Doe'];
const reg = new RegExp(/\[\[(.*?)\]\]/); // Match text inside two square brackets

formatMentionText(text, values, reg);
// Lorem ipsum dolor sit amet <mark>Jonh Doe</mark> and <mark>Jane Doe</mark>

I had a similar issue and I solved it with react portals, to easily find the node, I replaced the string with a div and rendered my react component using createPortal into that div.

@rmtngh I'm curious why you'd need to do that. It seems like it might be overkill. Are you interlacing non-react code?

Regarding solutions to this problem, I'd like to re-recommend string-replace-to-array (I'm the author). It's been battle and time-tested on multiple environments. It's also quite small (the entire library is this file https://github.com/oztune/string-replace-to-array/blob/master/string-replace-to-array.js).

Here's a usage example with regex:

import replace from 'string-replace-to-array'

// The API is designed to match the native 'String.replace',
// except it can handle non-string replacements.
replace(
  'Hello Hermione Granger...',
  /(Hermione) (Granger)/g,
  function (fullName, firstName, lastName, offset, string) {
    return <Person firstName={ firstName } lastName={ lastName } key={ offset } />
  }
)

// output: ['Hello ', <Person firstName="Hermione" lastName="Granger" key={ 0 } />, ...]

@oztune Thanks for your reply, In this case I have a string of html markup coming from a wysiwyg editor of a CMS and I'd like to place react components in certain places inside that markup.
Do you think it would be better to use a library instead of using portals? I think what portal does is to just render the component inside another node instead of nearest node which wouldn't be more expensive than rendering a normal component and the only extra step is to replace the string which can be done with native js replace.
Am I missing anything? Will there be any advantages in using the library instead?

@rmtngh I'd say it depends on what you're trying to do. If you're just trying to sprinkle some React components in different areas of your DOM tree, and the tree isn't already being rendered with React, portals are the way to go. The method I mentioned is most useful when the parts you're trying to replace are already rendered inside of a React component.

Here's the code that returns an array of nodes given text and pattern:

const highlightPattern = (text, pattern) => {
  const splitText = text.split(pattern);

  if (splitText.length <= 1) {
    return text;
  }

  const matches = text.match(pattern);

  return splitText.reduce((arr, element, index) => (matches[index] ? [
    ...arr,
    element,
    <mark>
      {matches[index]}
    </mark>,
  ] : [...arr, element]), []);
};

Thank you @wojtekmaj
My version for groups in pattern:

const replacePatternToComponent = (text, pattern, Component) => {
  const splitText = text.split(pattern);
  const matches = text.match(pattern);

  if (splitText.length <= 1) {
    return text;
  }

  return splitText.reduce((arr, element) => {
      if (!element) return arr;

      if(matches.includes(element)) {
        return [...arr, Component];
      }

      return [...arr, element];
    },
    []
  );
};

const string = 'Foo [first] Bar [second]';
const pattern = /(\[first\])|(\[second\])/g;

replacePatternToComponent(string, pattern, <Component />);

Approach using typescript:

const formatString = (
  str: string,
  formatingFunction?: (value: number | string) => React.ReactNode,
  ...values: Array<number | string>
) => {
  const templateSplit = new RegExp(/{(\d)}/g);
  const isNumber = new RegExp(/^\d+$/);
  const splitedText = str.split(templateSplit);
  return splitedText.map(sentence => {
    if (isNumber.test(sentence)) {
      const value = values[Number(sentence)];
      return Boolean(formatingFunction) ? formatingFunction(value) : value;
    }
    return sentence;
  });
};

Example of use:

const str = '{0} test {1} test';
formatString(str, value => <b>{value}</b>, value1, value2);

Result:
<b>value1</b> test <b>value2</b> test

@rmtngh could you provide a sample of how you achieved the markup enrichment through createportal? I'm looking to do the same.

@Dashue The idea is to create some targets for your react components inside your markup, depending on the HTML string and how much control you have over it,
If you can change it on the server side, you may want to give an ID to your elements. Something like :
<div id="component_target_1"></div>
And inside react, look for \id="(component_target_\d)"\g with regexp, store the ids (state.targets) and render your components:

let mappedComponents
if(this.state.targets && this.state.targets.length){
            mappedComponents = this.state.targets.map((id,index)=><MyComponent id={id} key={id} />)
        }

Here is a sample of "MyComponent":

import React from 'react'
import ReactDOM from 'react-dom'

export default class MyComponent extends React.Component {
  constructor(props) {
    super(props)
  }
  getWrapper(){
    return document.getElementById(this.props.id)
  }
  render() {
    if(!this.getWrapper()) return null

    const content = <div>
        Hello there! I'm rendered with react.
      </div>

      return ReactDOM.createPortal(
        content,
        this.getWrapper(),
      );
  }
}

If you don't have much control over the HTML string, you still can use the same approach, you might need to find some elements and inject a target element into the string.

Great thread guys!

I created a simple React Text Highlighter pen that implements some of the ideas in this thread (with some extra magic...), hopefully future visitors can find it useful.

Here's a solution using map,
forking @sophiebits solution here:

const reg = new RegExp(/(\bmoo+\b)/, 'gi');
const parts = text.split(reg);
return <div>{parts.map(part => (part.match(reg) ? <b>{part}</b> : part))}</div>;

Beautiful solution @rehat101

Here's what worked best in my case -- splitting by the space character and looking for specific words.

It's a little messy in the HTML but who cares 😂

💗 RegExp and map make this soooo clean. 💗💗💗

image

const Speech = ({ speech, speeches, setSpeeches, i, text }) => {

const highlightRegExp = new RegExp(
    /you|can|do|it|puedes|hacerlo|pingüinos|son|bellos/,
    "gi"
  );
  const delineator = " ";
  const parts = text.split(delineator);

  return (
        <div>
          {parts.map(part =>
            part.match(highlightRegExp) ? <b>{part + " "}</b> : part + " "
          )}
        </div>
  );
};

Beautiful @rehat101

If react-string-replace doesn't fit your requirements:

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

Brilliant! I find it extremely useful since most of the times you actually need to access the original string

I refactored the solution of @nullhook a little bit to make it accept variable from a query and memic the behavior of Google search autocomplete
js const highlightQueryText = (text: string, filterValue: string) => { const reg = new RegExp(`(${filterValue})`, 'gi'); const textParts = text.split(reg); return ( <span style={{ fontWeight: 600 }}> {textParts.map(part => (part.match(reg) ? <span style={{ fontWeight: 'normal' }}>{part}</span> : part))} </span> ); };

so I found a nice combination of implementing DOMPurify to sanitize html strings with html-react-parser to find targeted markup in it and convert it to JSX components - I use Rebass with passed props as an example.. that came out quite nicely, the two packages will help you to avoid the overhead of rolling your own parser, I just wouldnt recommend this parser package without sanitization. For those struggling with this still maybe scope out this codepen for what it's worth: https://codesandbox.io/s/eager-wiles-5hpff. Feedback on any improvements much appreciated too.

Hey Jesse,
That seems like a really nice combo for taking text and transforming it to
React components! It would definitely be good for replacing parts of a
string with a component.

Thanks for sharing.

Could also be useful in a JAMstack site to turn Markdown into components.

Best regards,
Derek

Derek R. Austin, PT, DPT, MS, BCTMB, LMT, CSCS

Join me on LinkedIn: https://www.linkedin.com/in/derek-austin/

On Wed, Feb 26, 2020, 4:35 PM Jesse Lewis notifications@github.com wrote:

so I found a nice combination of implementing DOMPurify to sanitize html
strings with html-react-parser to find targeted markup in it and convert it
to JSX components - I use Rebass with passed props as an example.. that
came out quite nicely, the two packages will help you to avoid the overhead
of rolling your own parser, I just wouldnt recommend this parser package
without sanitization. For those struggling with this still maybe scope out
this codepen for what it's worth:
https://codesandbox.io/s/eager-wiles-5hpff. Feedback on any improvements
much appreciated too.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/facebook/react/issues/3386?email_source=notifications&email_token=AMV42QTV4FDXZIFXCGB3NZDRE3OATA5CNFSM4A5VRBI2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOENB7H7Q#issuecomment-591655934,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AMV42QXARIOKOYSJ4C5IFQLRE3OATANCNFSM4A5VRBIQ
.

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
  parts[i] = <span className="match" key={i}>{parts[i]}</span>;
}
return <div>{parts}</div>;

Let me know if that's unclear.

This works great. Any way to get it to work in Angular? I've posted a question about it, using your code:
https://stackoverflow.com/questions/60889171/wrap-certain-words-with-a-component-in-a-contenteditable-div-in-angular

Hello friends, I need your help please, I am using React Native and what I need to do is from a text that I extract from the DB to apply a format (font color, link) to make @mentions, when searching if in the text finds 1 single match makes replacement all good, but! If there are several @mentions in the text, it throws me an error.

/////Text example: _hey what happened @-id:1- everything ok ?? and you @-id:2- @-id:3- @-id:4-_
//// listusers its array, example: [idusuario: "1", usuario: "@luigbren", format: "@-id:1-".....]

const PatternToComponent = (text, usuarios,) => {
    let mentionsRegex = new RegExp(/@-(id:[0-9]+)-/, 'gim');
    let matches = text.match(mentionsRegex);
    if (matches && matches.length) {
        matches = matches.map(function (match, idx) {
            let usrTofind = matches[idx]
            //////////////////////////////////////////////////////////////////
            const mentFormat = listusers.filter(function (item) { 
                const itemData = item.format;
                return itemData.indexOf(usrTofind) > -1;
            });
            if (mentFormat.length > 0) {
                let idusuario = mentFormat[0].idusuario
                let replc = mentFormat[0].usuario

                console.log(usrTofind) //// here find @-id:1-
                console.log(replc)   //// here is @luigbren for replace

                ////////// now here replace part of the string, @-id:1- with a <Text> component 
                ///////// with @luigbren and the link, this is repeated for every @mention found

                parts = text.split(usrTofind);
                for (var i = 1; i < parts.length; i += 2) {
                  parts[i] = <Text key={i} style={{ color: '#00F' }} onPress={() => { alert('but this is'); }}>{replc}</Text>;
                }
                return text = parts;
                /////////////////////////////////////////////////////////////////////
            } else {
                return text
            }
        });
    } else {
        return text
    }
    return text
};

in this way the code works well for me only when in the text there is only one mention, example '_hey what happened @-id:1- everything ok ??_' , but when placing more than one mention it gives me an error, example: '_hey what happened @-id:1- everything ok ?? @-id:2- @-id:3-_' ... Error: TypeError: text.split is not a function and if instead of placing _parts = text.split(usrTofind);_ I place _parts = text.toString().split(usrTofind);_ it gives me an [Object Object] error

You can use html-react-parser to parse the string, then replace by node name with jsx components. This type of replacement will allow you to use react elements dynamically by replacing strings.

      parse(body, {
        replace: domNode => {
          if (domNode.name === 'select') { // or
            return React.createElement(
              Select, // your react component
              { },
              domToReact(domNode.children)
            );
          }
        }

Someone tell me how can replace react-router-dom in string?

Example:

var text = "Are You okay @sara ?";

var href= <Link to={{
  pathname: '/user/sara',
}}> @sara </Link>;

var replace = text.replace("@sara", href);

//output : Are You okey [Object Object] ?

i realy don't know who is [Object Object] ? i say "Are you Okay @sara", but this code called someone else !

I have almost the same question in react-native, I appreciate if anyone can help me

my question

Nothing worked for me except this package https://www.npmjs.com/package/regexify-string

I am using this function:

export const replaceWithHtml = (
  text: string,
  textToReplace: string,
  replaceValue: any,
): any[] => {
  const delimiter = '|||||'
  return text && text.includes(textToReplace)
    ? text
        .replace(textToReplace, `${delimiter}${textToReplace}${delimiter}`)
        .split(delimiter)
        .map((i) => {
          if (i === textToReplace) {
            return replaceValue
          } else {
            return i || null
          }
        })
    : text
}

Like that:

const text = 'This is an [icon]'
replaceWithHtml(
      text,
      '[icon]',
      <i className="icon-cogs" />,
)

I just wanted a simple interpolation tool, to replace certain keywords with certain elements.

To reduce the number of elements in my output I'm using a React.Fragment.

const interpolate = (text, values) => {
  const pattern = /([$0-9]+)/g
  const matches = text.match(pattern)
  const parts = text.split(pattern)

  if (!matches) {
    return text
  }

  return parts.map((part, index) => (
    <Fragment key={part + index}>{matches.includes(part) ? values[part] : part}</Fragment>
  ))
}

// ...

<p>
  {interpolate('$1 with $2 is fun!', {
    $1: <em>Playing</em>,
    $2: <strong>JavaScript</strong>,
  })}
</p>

// <p><em>Playing</em> with <strong>JavaScript</strong> is fun!</p>

Thanks @sophiebits for the idea on split() 🙏

Based on @pedrodurek's solution, here is something for my specific needs (translations with readable keys instead of just numbers):

export const insertComponentsIntoText = (
    str: string,
    replacements: {
        [key: string]: React.ReactNode
    }
) => {
    const splitRegex = new RegExp(/\[\[(\w*)\]\]/g);
    const parts = str.split(splitRegex);
    return parts.map(part => {
        if (replacements.hasOwnProperty(part)) {
            return replacements[part];
        }
        return part;
    });
};

Example:

insertComponentsIntoText(`"Please accept our [[privacyPolicyLink]] and [[termsLink].", {
    "privacyPolicyLink": <a key="privacyPolicyLink" href="/privacy">Privacy Policy</a>,
    "termsLink": <a key="termsLink" href="/terms">Terms and Conditions</a>
})

...becomes...

Please accept our <a key="privacyPolicyLink" href="/privacy">Privacy Policy</a> and <a key="termsLink" href="/terms">Terms and Conditions</a>.

(The keys are necessary because it actually becomes an array, and react doesn't like array elements without keys.)

Nothing worked for me except this package https://www.npmjs.com/package/regexify-string

Me too. Tried all of them only this package works. Thank you!

Was this page helpful?
0 / 5 - 0 ratings