emil.c
emil.c

Reputation: 2018

Render custom React component within HTML string from server

I have a HTML string that comes from the server, for example:

const myString = '<p>Here goes the text [[dropdown]] and it continues</p>`;

And I split this string into 3 parts so the result is the following:

const splitString = [
  '<p>Here goes the text ',
  '[[dropdown]]',
  ' and it continues</p>'
];

Then I process those 3 parts in order to replace the dropdown with a React component:

const processedArr = splitString.map((item) => {
  if (/* condition that checks if it's `[[dropdown]]` */) {
    return <Dropdown />;
  }
  return item;
}

So after all, I get the processed array, which looks like this:

['<p>Here goes the text ', <Dropdown />, ' and it continues</p>']

When I render that, it renders the HTML as a text (obviously) with the Dropdown component (that renders properly) in between the text. The problem here is that I cannot use { __html: ... } because it has to be used such as <div dangerouslySetInnerHTML={{ __html: ... }} />. I cannot add <div> around the string because that would cut out the <p> tag.

I thought about splitting the parts into tags and then in some sort of loop doing something like:

React.createElement(tagName, null, firstTextPart, reactComponent, secondTextPart);

but that would require fairly complex logic because there could be multiple [[dropdown]]s within one <p> tag and there could be nested tags as well.

So I'm stuck right now. Maybe I'm looking at the problem from a very strange angle and this could be accomplished differently in React. I know that React community discourages rendering HTML from strings, but I cannot go around this, I always have to receive the text from the server.

The only stackoverflow question I found relevant was this one, however that supposes that content coming from backend has always the same structure so it cannot be used in my case where content can be anything.

EDIT: After some more digging, I found this question and answer which seems to be kinda solving my problem. But it still feels a bit odd to use react-dom/server package with its renderToString method to translate my component into a string and then concatenate it. But I'll give it a try and will post more info if it works and fits my needs.

Upvotes: 2

Views: 1106

Answers (3)

sashok1337
sashok1337

Reputation: 1019

Since WebComponents are supported by all modern browsers, I decided to publish an additional answer on how this can be done almost natively.

It will be much faster because you don't need to modify the HTML string (or go through the nodes) in any way. And it will require only additional 10-15 lines of code and 0 dependencies. And you even can pass any parameters to your components. And use nested components as well.

The idea is pretty simple - we need to define WebComponent inside react component class:

class DropdownComponent extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    ReactDOM.render(<Dropdown />, this);
  }

  disconnectedCallback() {
    ReactDOM.unmountComponentAtNode(this);
  }
}

Then define it with:

customElements.define('dropdown-component', DropdownComponent);

And then you can insert your trusted HTML code like this:

class App extends Component {
  render() {
    // we need to render existing react component from the string
    const myTrustedHtmlString = '<p>Here goes the text <dropdown-component></dropdown-component> and it continues</p>';

    return <div dangerouslySetInnerHTML={{ __html: myTrustedHtmlString }}></div>;
  }
}

You can find working example here (with passing parameters to a component, as well as using a nested component).

Upvotes: 0

Alex Yan
Alex Yan

Reputation: 2385

How about break the text apart and render the component separately?

your react component should look like this (in JSX):

<div>
    <span>first text</span>
    {props.children} // the react component you pass to render
    <span>second part of the text</span>
</div>

and you would just call out this component with something like:

<MessageWrapper>
    <DropdownComponent/> // or whatever
</MessageWrapper>

Upvotes: 0

emil.c
emil.c

Reputation: 2018

So after playing with the code, I finally came to a "solution". It's not perfect, but I haven't found any other way to accomplish my task.

I don't process the splitString the way I did. The .map will look a bit different:

// Reset before `.map` and also set it up in your component's constructor.
this.dropdownComponents = [];

const processedArr = splitString.map((item) => {
  if (/* condition that checks if it's `[[dropdown]]` */) {
    const DROPDOWN_SELECTOR = `dropdown-${/* unique id here */}`;
    this.dropdownComponents.push({
      component: <Dropdown />,
      selector: DROPDOWN_SELECTOR
    });
    return `<span id="${DROPDOWN_SELECTOR}"></span>`;
  }
  return item;
}).join('');

Then for componentDidMount and componentDidUpdate, call the following method:

  _renderDropdowns() {
    this.dropdownComponents.forEach((dropdownComponent) => {
      const container = document.getElementById(dropdownComponent.selector);
      ReactDOM.render(dropdownComponent.component, container);
    });
  }

It will make sure that what's within the span tag with a particular dropdown id will be replaced by the component. Having above method in componentDidMount and componentDidUpdate makes sure that when you pass any new props, the props will be updated. In my example I don't pass any props, but in real-world example you'd normally pass props.

So after all, I didn't have to use react-dom/server renderToString.

Upvotes: 1

Related Questions