16

I used to compile and insert JSX components via

<div key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />

which wrapped my HTML into a <div>:

<div>my html from the HTML object</div>

Now react > 16.2.0 has support for Fragments and I wonder if I can use that somehow to avoid wrapping my HTML in a <div> each time I get data from the back end.

Running

<Fragment key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />

will throw a warning

Warning: Invalid prop `dangerouslySetInnerHTML` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.
in React.Fragment

Is this supported yet at all? Is there another way to solve this?

Update

Created an issue in the react repo for it if you want to upvote it.

Dominik
  • 6,078
  • 8
  • 37
  • 61

5 Answers5

5

Short Answer

Not possible:

key is the only attribute that can be passed to Fragment. In the future, we may add support for additional attributes, such as event handlers.

https://reactjs.org/docs/fragments.html

You may want to chime in and suggest this as a future addition.

https://github.com/facebook/react/issues

In the Meantime

You may want to consider using an HTML parsing library like:

https://github.com/remarkablemark/html-react-parser

Check out this example to see how it will accomplish your goal:

http://remarkablemark.org/blog/2016/10/07/dangerously-set-innerhtml-alternative/

In Short

You'll be able to do this:

<>
{require('html-react-parser')(
    '<em>foo</em>'
)}
</>
Community
  • 1
  • 1
Jonny Asmar
  • 1,900
  • 14
  • 16
5

Update December 2020

This issue (also mentioned by OP) was closed on Oct 2, 2019. - However, stemming from the original issue, it seems a RawHTML component has entered the RFC process but has not reached production, and has no set timeline for when a working solution may be available.

That being said, I would now like to allude to a solution I currently use to get around this issue.

In my case, dangerouslySetInnerHTML was utilized to render plain HTML for a user to download; it was not ideal to have additional wrapper tags included in the output.

After reading around the web and StackOverflow, it seemed most solutions mentioned using an external library like html-react-parser.

For this use-case, html-react-parser would not suffice because it converts HTML strings to React element(s). Meaning, it would strip all HTML that wasn't standard JSX.


Solution:

The code below is the no library solution I opted to use:

//HTML that will be set using dangerouslySetInnerHTML
const html = `<div>This is a div</div>`

The wrapper div within the RawHtml component is purposely named "unwanteddiv".

//Component that will return our dangerouslySetInnerHTML
//Note that we are using "unwanteddiv" as a wrapper
    const RawHtml = () => {
        return (
            <unwanteddiv key={[]}
                dangerouslySetInnerHTML={{
                    __html: html,
                }}
            />
        );
    };

For the purpose of this example, we will use renderToStaticMarkup.

const staticHtml = ReactDomServer.renderToStaticMarkup(
<RawHtml/>
);

The ParseStaticHtml function is where the magic happens, here you will see why we named the wrapper div "unwanteddiv".

  //The ParseStaticHtml function will check the staticHtml 
  //If the staticHtml type is 'string' 
  //We will remove "<unwanteddiv/>" leaving us with only the desired output
    const ParseStaticHtml = (html) => {
        if (typeof html === 'string') {
            return html.replace(/<unwanteddiv>/g, '').replace(/<\/unwanteddiv>/g, '');
        } else {
            return html;
        }
  };

Now, if we pass the staticHtml through the ParseStaticHtml function you will see the desired output without the additional wrapper div:

  console.log(ParseStaticHtml(staticHtml));

Additionally, I have created a codesandbox example that shows this in action.

Notice, the console log will throw a warning: "The tag <unwanteddiv> is unrecognized in this browser..." - However, this is fine because we intentionally gave it a unique name so we can easily differentiate and target the wrapper with our replace method and essentially remove it before output.

Besides, receiving a mild scolding from a code linter is not as bad as adding more dependencies for something that should be more simply implemented.

Aib Syed
  • 3,118
  • 2
  • 19
  • 30
2

i found a workaround
by using react's ref

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

interface RawHtmlProps {
    html: string
}

const RawHtml: FC<RawHtmlProps> = ({ html }) => {
    const ref = useRef<HTMLDivElement>(null)

    useEffect(() => {
        if (!ref.current) return

        // make a js fragment element
        const fragment = document.createDocumentFragment()

        // move every child from our div to new fragment
        while (ref.current.childNodes[0]) {
            fragment.appendChild(ref.current.childNodes[0])
        }

        // and after all replace the div with fragment
        ref.current.replaceWith(fragment)
    }, [ref])

    return <div ref={ref} dangerouslySetInnerHTML={{ __html: html }}></div>
}

export { RawHtml }
007
  • 86
  • 1
  • 5
1

A trick (bypassing React) is to create a component with a ref and when it mounts, replace the dummy element's HTML entirely using outerHTML.

The (below) HTMLString component is blocked from re-renders by being wrapped with a memo which has a custom compare function which always returns true forcing it to think props never change (even if they might)

const HTMLString = `<svg viewBox="0 0 100 100" width='100' xmlns="http://www.w3.org/2000/svg">
  <path d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" />
</svg>`

// good use case for "memo" as this should NEVER re-render
const HTML = React.memo(({value}) => {
  const ref = React.useRef();

  // replace the "placeholder" component once and never again
  // (important to not have ANY deps)
  React.useEffect(() => ref.current.outerHTML = value, []) 

  // placeholder component which will be entirely replaced
  return <a ref={ref}/>;
}, () => true);


// Render 
ReactDOM.render(<HTML value={HTMLString}/>, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

This solves the problem of having a useless HTML element as a wrapper when using dangerouslySetInnerHTML (which can also cause CSS issues because no wrapper is expected)

vsync
  • 118,978
  • 58
  • 307
  • 400
  • This solution does not work with React 18 – jpcamara Jul 05 '23 at 13:10
  • @jpcamara - Tested on React 18 before posting - [see codesandbox](https://codesandbox.io/s/react-render-string-as-html-vtk2qh) – vsync Jul 05 '23 at 14:09
  • ah I see the problem - I always run my examples in strict mode. In react 17, your example works fine in strict mode. In react 18, it breaks. So if someone is ok not running their code in strict mode this is a potential solution, but if you run in strict mode it isn't working (in my testing, including your code sandbox) – jpcamara Jul 05 '23 at 14:27
  • 1
    it is a recommendation of the react team. https://react.dev/reference/react/StrictMode. "We recommend wrapping your entire app in Strict Mode, especially for newly created apps. If you use a framework that calls createRoot for you, check its documentation for how to enable Strict Mode." – jpcamara Jul 05 '23 at 19:22
  • 1
    You certainly don't. For anyone checking this solution, if you are using any kind of react meta framework like next.js, it won't work out of the box, because most meta frameworks enable strict mode by default. If you want to explicitly turn off strict mode, it will work (at least for now, up until React 18). The react team does not recommend it, but vsync does. – jpcamara Jul 07 '23 at 13:00
0

Here's a solution that works for <td> elements only:

type DangerousHtml = {__html:string}

function isHtml(x: any): x is DangerousHtml {
    if(!x) return false;
    if(typeof x !== 'object') return false;
    const keys = Object.keys(x)
    if(keys.length !== 1) return false;
    return keys[0] === '__html'
}

const DangerousTD = forwardRef<HTMLTableCellElement,Override<React.ComponentPropsWithoutRef<'td'>,{children: ReactNode|DangerousHtml}>>(({children,...props}, ref) => {
    if(isHtml(children)) {
        return <td dangerouslySetInnerHTML={children} {...props} ref={ref}/>
    }
    return <td {...props} ref={ref}>{children}</td>
})

With a bit of work you can make this more generic, but that should give the general idea.

Usage:

<DangerousTD>{{__html: "<span>foo</span>"}}</DangerousTD>
mpen
  • 272,448
  • 266
  • 850
  • 1,236