64

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.

Shane Cavaliere
  • 2,175
  • 1
  • 17
  • 18
  • I'm not sure if this'll help but: https://facebook.github.io/react/tips/dangerously-set-inner-html.html and http://stackoverflow.com/questions/21285262/react-leave-the-contents-of-a-component-alone – lux May 06 '16 at 23:58
  • 1
    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. – Shane Cavaliere May 07 '16 at 00:08
  • 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: http://codepen.io/mikechabot/pen/xVMwgN?editors=0011 As you can see, we're able to dump `"
    Hello, World.
    "` 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.
    – lux May 07 '16 at 00:46
  • @ShaneCavaliere, i ended up at this questions trying to do the same thing with mapbox GL. do you recall what you ended up doing? – imjared Aug 18 '16 at 06:40
  • 5
    @imjared Yes, I did end up using `ReactDOMServer.renderToString()` and it's been working fine. I haven't noticed any issues so far. – Shane Cavaliere Aug 18 '16 at 06:45
  • 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` https://facebook.github.io/react/docs/top-level-api.html#reactdomserver.rendertostaticmarkup if you will not be needing the generated HTML to have React's extra DOM attributes etc. – Dave Aug 24 '16 at 02:21
  • 5
    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. – Shane Cavaliere Aug 24 '16 at 02:27
  • 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. – Eric Burel Jul 17 '20 at 16:38

4 Answers4

37

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()
ajorquera
  • 1,297
  • 22
  • 29
Thomas Grainger
  • 2,271
  • 27
  • 34
  • 1
    When I try to use renderToStaticMarkup in the browser it gives me an unresolved package error after transpiling, saying it's looking for "stream". – ADJenks Dec 23 '21 at 21:59
  • @ADJenks if you get this error, try to include this polyfill https://github.com/FredKSchott/rollup-plugin-polyfill-node. That worked for me – ajorquera May 03 '23 at 16:04
20

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
});
Mohamed Shaaban
  • 1,129
  • 6
  • 13
  • 5
    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))` – styfle Nov 10 '17 at 17:56
  • 1
    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? – Rehan Umar Mar 12 '21 at 12:02
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>;
};
Christian Fritz
  • 20,641
  • 3
  • 42
  • 71
  • 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: https://stackoverflow.com/questions/68564210/rollup-esm-generates-broken-imports – ADJenks Dec 23 '21 at 22:00
  • this will produce a useless `div` wrapper to the already-existing `div` in the `html` string. – vsync Jul 05 '23 at 08:52
1

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>
  • Is there any way to accomplish this in React 18 where the `renderToString` and `renderToStaticMarkup` were removed when running in the browser? – scott dickerson Sep 22 '22 at 14:53
  • @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? – Robert Aron Sep 24 '22 at 20:31