35
votes

Currently the render method can only return a single element/component. See: here

In the discussion under that ticket some suggest to wrap multiple elements returned from a React component in a HTML comment so that the wrapping component is ignored by the browser, e.g.:

<A>
    <B></B>
    <Fragment>
        <C></C>
        <D></D>
    </Fragment>
    <E></E>
</A>

would render to:

<a>
    <b></b>
    <!--<fragment data-reactid="">-->
        <c></c>
        <d></d>
    <!--</fragment>-->
    <e></e>
</a>

But how to actually create a component that renders just HTML comment? In other words, how the render function of the 'fragment' component in the example above could look like?

7
The person posting that comment didn't understand how React works. Note that none of the ones who do suggested it would work. For one thing, it doesn't address the core issue; the result is four nodes (a comment node, two element nodes, and then a comment node), not a single node. - T.J. Crowder
My understanding was that the Fragment's render function would only return the Fragment component with two children components 'c' and 'd'. Therefore the closing tag '/fragment' in the second comment. Also, it seems that technique has been used to implement a fragment component in mwiencek/react fork in commit dcc972c414, but I might be wrong. - Greg
Hey @Greg, I hope my solution helps. Sorry that I had to edit/refactor it a few times. I apologize if you were notified too many times, while I made all these changes. - kronus

7 Answers

25
votes

This is what I have ended up with in one of my recent projects:

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

class ReactComment extends Component {
    static propTypes = {
        text: PropTypes.string,
        trim: PropTypes.bool
    };

    static defaultProps = {
        trim: true
    };

    componentDidMount() {
        let el = ReactDOM.findDOMNode(this);
        ReactDOM.unmountComponentAtNode(el);
        el.outerHTML = this.createComment();
    }

    createComment() {
        let text = this.props.text;

        if (this.props.trim) {
            text = text.trim();
        }

        return `<!-- ${text} -->`;
    }

    render() {
        return <div />;
    }
}

export default ReactComment;

So you can use it like this:

<A>
    <B></B>
    <ReactComment text="<fragment>" />
        <C></C>
        <D></D>
     <ReactComment text="</fragment>" />
    <E></E>
</A>
7
votes

Here's another novel approach if you need this to work with SSR.

Here's a MaxWidth component I am using with my react-based email tool called Myza.

import ReactDOMServer from 'react-dom/server'

export const MaxWidth = ({ maxWidth = 0, className, children }: IMaxWidthProps) => {
  const renderedChildren = ReactDOMServer.renderToStaticMarkup(
    <div className={className} style={{ maxWidth: `${maxWidth}px`, margin: '0 auto' }}>
      {children}
    </div>
  )

  return <div dangerouslySetInnerHTML={{
    __html: `
    <!--[if mso]><center><table><tr><td width="${maxWidth}"><![endif]-->
    ${renderedChildren}
    <!--[if mso]> </td></tr></table></center><![endif]-->
  ` }}
  />
}
2
votes

You can do it with the following component, it is simple and functional but it has the drawback of having to wrap your comment in a HTML node i.e. a "div" because it makes use of the dangerouslySetInnerHTML attribute:

    const ReactComment = ({ text }) => {
  return <div dangerouslySetInnerHTML={{ __html: `<!-- ${text} -->` }}/>
}

Then, you use it like so:

<ReactComment text={'My beautiful HTML comment'}/>
1
votes

Assuming you are on React 16.8+ you may use a small functional component which lets you provide a text property and render an html comment.

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

const ReactComment = ( props ) => {
    const el = useRef();
    useEffect( () => {
        el.current.outerHTML = `<!-- ${props.text} -->`;
    }, [] );
    return (
        <div ref={el}/>
    );
};

export default ReactComment;

You may then use it like so

<A>
    <B></B>
    <ReactComment text="<fragment>" />
        <C></C>
        <D></D>
     <ReactComment text="</fragment>" />
    <E></E>
</A>
1
votes

HTML Comments in React

To render comments in React (which is what I'm guessing most people are looking for when they come to this question), I use a react component which I have in a gist. It was based off of the answer by Alex Zinkevych, but with the following improvements:

  • Updates to props now trigger the component to update, so the comment can be more dynamic
  • The component cleans up after itself
  • The div is hidden before being swapped out for the comment node
  • (Code Style) React Ref used instead of ReactDOM.findDOMNode(this), which is the recommended way of interacting with the DOM elements, according to React's documentation.

I linked to the gist above, but I've also copied the content at the time of this writing below, but you might want to see if there's any revisions on the gist, since I will fix any bugs I might find and post as revisions to the Gist.

import * as React from 'react';
import * as ReactDOM from 'react-dom';

interface IProps {
    text: string;
}

export class HTMLComment extends React.Component<IProps> {
    private node: Comment;
    private ref$rootDiv = React.createRef<HTMLDivElement>();

    constructor(props: IProps) {
        super(props);

        this.node = window.document.createComment(props.text);
    }

    componentDidMount() {
        if (this.ref$rootDiv && this.ref$rootDiv.current) {
            let divElement = this.ref$rootDiv.current;

            // Tell React not to update/control this node
            ReactDOM.unmountComponentAtNode(divElement);

            // Replace the div with our comment node
            this.ref$rootDiv.current.replaceWith(this.node);
        }
    }

    componentDidUpdate(prevProps: IProps) {
        if (prevProps.text !== this.props.text) {
            this.node.textContent = this.props.text;
        }
    }

    componentWillUnmount() {
        this.node.remove();
    }

    render() {
        return (
            <div
                ref={this.ref$rootDiv}
                style={{
                    display: 'none',
                }}
            />
        );
    }
}

Answering the Actual Question

However, as the OP noted in a comment on Alex's post, this doesn't actually answer the question. For a single component that renders comments before and after the children, we can use the HTMLComment component defined above and compose a new component:

interface IHTMLCommentWrapperProps {

}

const HTMLCommentWrapper: React.FunctionComponent<IHTMLCommentWrapperProps> = (props) => {
    return (
        <React.Fragment>
            <HTMLComment text={`<fragment data-reactid="">`} />
            {props.children}
            <HTMLComment text={`</fragment>`} />
        </React.Fragment>
    )
}

Now, we can put all this together into one script. Here is that source code over at the Typescript playground, as well as a Gist (it's large and repeast the components detailed above, so I won't copy that code directly into this answer.

We can copy the compiled javascript into the snippet below:

class HTMLComment extends React.Component {
    constructor(props) {
        super(props);
        this.ref$rootDiv = React.createRef();
        this.node = window.document.createComment(props.text);
    }
    componentDidMount() {
        if (this.ref$rootDiv && this.ref$rootDiv.current) {
            let divElement = this.ref$rootDiv.current;
            // Tell React not to update/control this node
            ReactDOM.unmountComponentAtNode(divElement);
            // Replace the div with our comment node
            this.ref$rootDiv.current.replaceWith(this.node);
        }
    }
    componentDidUpdate(prevProps) {
        if (prevProps.text !== this.props.text) {
            this.node.textContent = this.props.text;
        }
    }
    componentWillUnmount() {
        this.node.remove();
    }
    render() {
        return (React.createElement("div", { ref: this.ref$rootDiv, style: {
                display: 'none',
            } }));
    }
}
const HTMLCommentWrapper = (props) => {
    return (React.createElement(React.Fragment, null,
        React.createElement(HTMLComment, { text: `<fragment data-reactid="">` }),
        props.children,
        React.createElement(HTMLComment, { text: `</fragment>` })));
};
const A = (props) => { return React.createElement("a", null, props.children); };
const B = (props) => { return React.createElement("b", null, props.children); };
const C = (props) => { return React.createElement("c", null, props.children); };
const D = (props) => { return React.createElement("d", null, props.children); };
const E = (props) => { return React.createElement("e", null, props.children); };
const App = () => {
    return (React.createElement(A, null,
        React.createElement(B, null),
        React.createElement(HTMLCommentWrapper, null,
            React.createElement(C, null),
            React.createElement(D, null)),
        React.createElement(E, null)));
};
let el$root = document.getElementById('react-app');
if (el$root) {
    ReactDOM.render(React.createElement(App, null), el$root);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

<div id="react-app"/>

If you run this snippet and inspect the HTML, you'll see the following:

enter image description here

0
votes

Create a functional component with a file name Comment.js

Import jquery to select divs using their class selectors, in combination with using native javascript document.createComment

Use props to pass the text to be used in the comments, as well as, the names of the divs to select:

import $ from 'jquery';

const Comment = (props) => {
  const commentMakerBegin = () => {
    setTimeout(() => {
      const beginComment = document.createComment(props.beginComment);
      const firstElement = $('.' + props.beforeDiv);
      firstElement.before(beginComment);
    }, 1000);
  };

  const commentMakerEnd = (event) => {
    setTimeout(() => {
      const endComment = document.createComment(props.endComment);
      const secondElement = $('.' + props.afterDiv);
      secondElement.after(endComment);
    }, 1000);
  };
  return (
    <>
      {commentMakerBegin()}
      {props.children}
      {commentMakerEnd()}
    </>
  );
};

export default Comment;

The props.children renders whatever is between your custom component tags:

{props.children}

Whether you type a string like 'Your components here' or '<C /><D />' it will render what you type between the opening and closing tags.

In the component that you would like to use the newly created Comment component, import it, followed by passing the text through props that you would like to use for the opening and closing comments

The following image is how I render comments before and after my two modals - consumer-modal and policy-modal respectively.

You can see in the console that the comments have been added

In my App.js file, I import the Comments component and use it in the following manner, which results in the aforementioned screen shot:

     <Comment
        beforeDiv='consumer-modal'
        afterDiv='policy-modal'
        beginComment='modal begins'
        endComment='modal ends'
      >
        <ConsumerModal
          title='testing'
          content={<ConsumerModalContent />}
          onClose={cnsmrModalHandler}
        ></ConsumerModal>
        <PolicyModal
          title='my policy'
          content={<PolicyModalContent />}
          onClose={policyModalHandler}
        />
      </Comment>
-11
votes

Edit: For those who found this answer useful, checkout this answer instead!

The posted problem is not asking for comment style in React!


Use curly brackets, such that you can use javascript comment /* */.

<a>
    <b></b>
    {/*<fragment data-reactid="">*/}
        <c></c>
        <d></d>
    {/*</fragment>*/}
    <e></e>
</a>