React.render replace container instead of inserting into

48.3k Views Asked by At

I'm gradually replacing some Backbone views with React.

My React View:

<div id="reactViewContent">Rendered By React</div>

I need to render the React view below other html elements without replacing them.

<div id="somestuff">
  <div id="anotherView">Other stuff not to delete</div>
  <div id="placeholderForReactView"></div>
</div>

I'd like that my method could replace the placeholder instead of inserting into it so that :

React.render(React.createElement(MyTestReactView), document.getElementById('placeholderForReactView'));

could result in:

<div id="somestuff">
  <div>Other stuff not to delete</div>
  <div id="reactViewContent">Rendered By React</div>
</div>

instead of:

<div id="somestuff">
  <div>Other stuff not to delete</div>
  <div id="placeholderForReactView">
    <div id="reactViewContent">Rendered By React</div>
  </div>
</div>

Without recurring at Jquery is there a correct way to do it?

7

There are 7 best solutions below

2
On BEST ANSWER

You don't need jQuery to work around this issue.

You just need to render into a temporary DIV and extract the content and replace the existing element. I've added the id="destination" so that the element can be easily retrieved from the temporary element.

var Hello = React.createClass({
    render: function() {
        return <div id="destination">Hello {this.props.name}</div>;
    }
});

// temporary render target
var temp = document.createElement("div");
// render
React.render(<Hello name="World" />, temp);
// grab the container
var container = document.getElementById("container");
// and replace the child
container.replaceChild(temp.querySelector("#destination"), document.getElementById("destination"));
0
On

After some research, I tried React Portal to better solve my issue, https://github.com/facebook/react/issues/20281#issuecomment-731134338

In your case the intention appears to be to render a subtree to another DOM root. That’s what portals do so they should help.

Solution:

const root = document.querySelector('#root');
const body = document.querySelector('body');

// Create an element
const temp = document.createElement('div');

// Create a portal, I want to insert child to root without 
// replacing the entire root container
const portal = ReactDOM.createPortal(<App />, root);

// Render the portal into temp element. 
// Inside the callback, we attach temp to the end of body 
// or, anywhere else inside the DOM tree
ReactDOM.render(portal, temp, () => body.appendChild(temp));

Result:

<html>
  <body>
    <div id="root">
      ... other elements ...

      <App />
    </div>
    <div></div> // our temp, acting as a portal. This can be re-located anywhere inside our DOM tree
  </body>
</html>

Previous solution (limited use cases only):

This works for me, create a temporary element and append it to root inside ReactDOM.render() callback.

const root = document.querySelector('#root');

const temp = document.createElement('div');
ReactDOM.render(
  <App />,
  temp,
  () => root.append(temp)
);
0
On

React 17

const rootDiv = document.createElement('div');

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootDiv,
  () => document.querySelector('#root').append(rootDiv)
);

React 18

import ReactDOM from 'react-dom/client';

function App({ callback }) {
  // Callback will be called when the div is first created.
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App callback={() => console.log('renderered')} />);

React official discussion https://github.com/reactwg/react-18/discussions/5#discussion-3385584

0
On

Expanding on @WiredPrairie's solution, the problem with using a placeholder element is the it seems React gets events from whatever the parent is, so if the parent isn't in the correct place in the DOM then events wont trigger (that's my theory anyhow).

To work around this you could use the actual elements parent, but keep the original DOM by keeping a copy of the parents children, then re-adding them later, something along these lines:

https://jsfiddle.net/n41dzLx2/

const el = document.querySelector("#app");
const parent = el.parentNode;
const oldChildren = Array.from(parent.childNodes);

ReactDOM.render(
  <App />, 
  parent, 
  () => {
    const newChildren = Array.from(parent.childNodes);
    parent.replaceChildren(...oldChildren);
    el.replaceWith(...Array.from(newChildren));
  }
)

This works for the most part, but the React elements will shift to the end of the parents elements if the root element within the component changes.

0
On

Unfortunately there is no way to do that currently with React. It replaces the contents of whatever element you render into.

The best you can do for now is render the React component separately and then mutate the DOM manually to get it where you want it.

I expect the functionality you seek will be available in React before long, but not yet!

1
On

This appears to work:

const MyComp = props=>{
  return <link href="https://fonts.googleapis.com/css?family=Cinzel&display=swap" rel="stylesheet"/>;
};

const myComp = MyComp({});

// when portal is "rendered", it will insert `myComp` into the specified container (document.head)
const portalThatInsertsMyCompIntoHead = ReactDOM.createPortal(myComp, document.head);

// render portal into unattached div container; portal still performs the insert-into-head as desired
ReactDOM.render(portalThatInsertsMyCompIntoHead, document.createElement("div"));

The result:

Note that, as desired, it doesn't delete the other children within document.head.

Not sure if this method has any negative side-effects, but it seems to work for my usages so far.

1
On

That should do it:

/**
 * Replaces html node with react component
 * @param element
 * @param reactComponent
 */
export function replaceNodeWithReactComponent (element: HTMLElement, reactComponent: any) {
  const parent = document.createElement('div');
  ReactDOM.render(reactComponent, parent, () => {
    element.replaceWith(...Array.from(parent.childNodes));
  });
}

Usage:

replaceNodeWithReactComponent(myHTMLelement, <MyReactComponent />);

EDIT: React 18

Since rendering was changed in react 18 and callback was removed, I managed to update replace method to following:

import ReactDOM from 'react-dom/client';

/**
 * Replaces html node with react component
 * @param element
 * @param reactComponent
*/
export function replaceNodeWithReactComponent (element: HTMLElement, reactComponent: React.ReactNode) {
  const parent = document.createElement('div');
  const root = ReactDOM.createRoot(parent);

  const callback = () => {  
    // Get nodes from parent div.
    const childNodes = Array.from(parent.childNodes || []);

    // Get nodes from rendered div under parent div.
    const flatChildNodes = childNodes.reduce((newFlatChildNodes, childNode) => {
      newFlatChildNodes.push(...Array.from(childNode.childNodes || []));

      return newFlatChildNodes;
    }, []);

    // Finally replace element with our react component.
    element.replaceWith(...flatChildNodes);
  };

  // Render and callback.
  root.render(<div ref={callback}>{reactComponent}</div>);
}

Make sure it the extension of this util file is tsx.