Is it ok to use ReactDOMServer.renderToString in the browser in areas where React isn't directly managing the DOM?
Asked Answered
C

4

68

I'm working on an app using Leaflet (via react-leaflet). Leaflet directly manipulates the DOM. The react-leaflet library doesn't change that, it just gives you React components that you can use to control your Leaflet map in a React-friendly way.

In this app, I want to use custom map markers that are divs containing a few simple elements. The way to do that in Leaflet is to set your marker's icon property to a DivIcon, in which you can set your custom HTML. You set that inner HTML by setting the DivIcon's html property to a string containing the HTML. In my case, I want that HTML to be rendered from a React component.

In order to do that, it seems like the correct approach is to use ReactDOMServer.renderToString() to render the Component that I want inside the map marker into a string, which I would then set as the html property of the DivIcon:

MyMarker.js:

import React, { Component } from 'react'
import { renderToString } from 'react-dom/server'
import { Marker } from 'react-leaflet'
import { divIcon } from 'leaflet'

import MarkerContents from './MarkerContents'

export class MyMarker extends Component {
  render() {
    const markerContents = renderToString(<MarkerContents data={this.props.data} />)
    const myDivIcon = divIcon({
      className: 'my-marker',
      html: markerContents
    })

    return (
      <Marker
        position={this.props.position}
        icon={myDivIcon} />
    )
  }
}

However, according to the React docs:

This [renderToString] should only be used on the server.

Is this a strict rule, or is it only meant to dissuade people from circumventing ReactDOM's efficient management of the DOM?

I can't think of another (better) way to accomplish what I'm after. Any comments or ideas would be greatly appreciated.

Chelsiechelsy answered 6/5, 2016 at 19:13 Comment(8)
I'm not sure if this'll help but: facebook.github.io/react/tips/dangerously-set-inner-html.html and #21285762Lupitalupo
Thanks, but I'm not actually trying to manually set innerHTML anywhere. I just want to prepare an HTML string from React components that I can use in a Leaflet DivIcon, and I'm wondering if there are any good reasons not to use ReactDOMServer.renderToString() to accomplish that. Definitely a good reminder to be diligent about sanitizing the HTML, though.Chelsiechelsy
Yeah, I suppose that's the polar opposite, huh? The only way I've been able to extract the pure HTML from a render is via the following: codepen.io/mikechabot/pen/xVMwgN?editors=0011 As you can see, we're able to dump "<div data-reactid='.0'>Hello, World.</div>" to console via refs, which is our component, but we needed to hook into the DOM in order to do so. I'd try to find a React dev on Twitter and pose this to them directly, otherwise, why not give it a shot? renderToString(), that is.Lupitalupo
@ShaneCavaliere, i ended up at this questions trying to do the same thing with mapbox GL. do you recall what you ended up doing?Stratiform
@Stratiform Yes, I did end up using ReactDOMServer.renderToString() and it's been working fine. I haven't noticed any issues so far.Chelsiechelsy
I can't speak to whether it is a good idea to use these on the client but for your use case, you may see better results with ReactDOM.renderToStaticMarkup facebook.github.io/react/docs/… if you will not be needing the generated HTML to have React's extra DOM attributes etc.Toreutics
My understanding is that ReactDOM.renderToStaticMarkup is best suited for static elements, and that the extra DOM attributes added by ReactDOMServer.renderToString are useful for React's internal DOM manipulation when re-rendering. Unless I'm mistaken, the best one to use probably depends on whether you anticipate updates occurring on that component in the future.Chelsiechelsy
Hi, for the record, in the scenarios you need this function client side and have many components to render, you might want to not use a React component at all, and instead create a string template, provided your logic is limited. I got a huge perf improvement in "react-leaflet-enhanced-marker" with this strategy for instance.Tasso
S
38

According to the new documentation: https://react.dev/reference/react-dom/server

The following methods can be used in both the server and browser environments:

  • renderToString()
  • renderToStaticMarkup()
Sacrosanct answered 1/2, 2018 at 15:2 Comment(2)
When I try to use renderToStaticMarkup in the browser it gives me an unresolved package error after transpiling, saying it's looking for "stream".Condemnation
@Condemnation if you get this error, try to include this polyfill github.com/FredKSchott/rollup-plugin-polyfill-node. That worked for meGrackle
B
23

I know it is too old question, but since it has not been answered I wanted to share my thoughts.

I was using the same thing, renderToString, but as the documentation recommends not to use it on client-side, I achieved it in another way, by using the react-dom's render method to render the custom component into div

var myDiv = document.createElement('div');

ReactDOM.render(
  <MarkerContents data={this.props.data} />,
  myDiv
);

var myIcon = L.divIcon({ 
    iconSize: new L.Point(50, 50), 
    html: myDiv.innerHTML
});
Bear answered 1/10, 2017 at 0:52 Comment(4)
I was doing this until React 16 started doing async rendering. The 3rd parameter to render must be a callback function before reading the innerHTML like so: ReactDOM.render(component, myDiv, () => console.log(myDiv.innerHTML))Miquelon
I have used it and now my event handlers are not called. By rendering the component in js object rather than dom. are we disturbing event binding for markerContent component?Brook
In case you would want to do the rendering synchronously from client, react recommends to to use flushSync instead. reference: react.dev/reference/react-dom/server/…Defence
Apparently we can't use this technique for whole documents <!DOCTYPE html><html>...</html> because it would replace the existing page with what you call render on. What I want is to generate the HTML string from a component, but don't want to insert it into the DOM, because I intend to put it in an iframe or save it to a file. In this case, it seems renderToStaticMarkup is the only way right now. renderToString can also work, but from what I read, it seems StaticMarkup is safer for this kind of use case.Castiglione
K
4

As Thomas already said, yes, you can use renderToString on the client. Just to be clear though, you will need to import ReactDOMServer on the client, which may seem counter-intuitive but appears to be correct. Example (on the client):

import React from 'react';
import ReactDOMServer from 'react-dom/server';

const MyComp = (props) => {
  const html = ReactDOMServer.renderToString(<div>{someFunc(props)}</div>);
  // do something with your html, then
  return <div dangerouslySetInnerHTML={{__html: html}}></div>;
};
Kalbli answered 30/10, 2020 at 16:44 Comment(2)
When I do this it gives me an error in the browser saying that it can't resolve "strream"... Basically a very similar problem to this guy here: #68564710Condemnation
this will produce a useless div wrapper to the already-existing div in the html string.Licensee
C
2

I had the exact same problem with leaflet and ended up solving the problem by thinking with portals.

import React, { useEffect, useMemo,useState } from "react";
import ReactDOM from "react-dom";
import { useLeafletContext } from "@react-leaflet/core";
import * as L from "leaflet";

/**
 * @type { React.FC<{
 *    positionX?: number
 *    positionY?: number
 *    width?: number
 *    height?:number
 * }> }
 * */
const LeafletChild = ({ children, positionX=0, positionY=0, width, height }) => {
  const context = useLeafletContext();
  const [divElement] = useState(() => document.createElement("div"));
  const icon = useMemo(() => L.divIcon({ iconSize: new L.Point(height, width), html: divElement }), [height,width, divElement]);
  const marker = useMemo(() => L.marker([0,0], { icon }), [icon]);
  useEffect(()=>{
    marker.setLatLng([positionY, positionX])
  },[positionY,positionX, marker])

  useEffect(() => {
    const container = context.layerContainer || context.map;
    container.addLayer(marker);
    return () => container.removeLayer(marker);
  }, [context, marker]);
  return ReactDOM.createPortal(children, divElement);
};

Somwhere else...

<LeafletChild>
  <div onClick={()=>setSomeBool(!someBool)}>
    anything you want here like...{`${someBool}`}
  </div>
</LeafletChild>
Cirone answered 27/9, 2021 at 21:8 Comment(2)
Is there any way to accomplish this in React 18 where the renderToString and renderToStaticMarkup were removed when running in the browser?Sargassum
@scottdickerson I think you meant to comment on another answer. This solution doesn't use either of those methods, which actually solves your problem I think?Cirone

© 2022 - 2024 — McMap. All rights reserved.