Skip to content
Advertisement

React component that’s rendered dynamically does not rerender on parent state changes

I have a component that I want to run through a non react animation library before render. This has prevented me from going the standard route of just using the standard hide/show logic. I initially tried to use ReactDOM.createPortal but that didn’t render the component at all. Using ReactDOM.render, I’ve gotten the element to render correctly upon completion of the animation and I’m able to successfully propagate changes up to the “parent” state but the state change doesn’t propagate back down to the “child”. Here’s my code:

Html

    <div id="root"></div>
    <div id="childPlaceholder"></div>

Javascript

import './App.css';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [data, updateData] = useState(0)
  function add(val) {
    console.log("add");
    updateData(val);
  }
  function renderSubpage() {


    let $el = document.getElementById("childPlaceholder");
    // NonReactAnimationLibrary.ShowContainer($el);
    ReactDOM.render(<Child number={data} add={add} />, $el);
    // ReactDOM.createPortal(<Child number={data} add={add} />, $el);

  }
  return ( <>
    <button onClick={renderSubpage}>
      add child
    </button>
    <div> data: {data}</div>
</>
  );
}
function Child(props) {
  return <>
  <button onClick={()=>{props.add(props.number + 1)}}>add number</button>
    <div>child {props.number}</div>
  </>
}

export default App;

Is it possible to do this in react?

Update 1:

So I’ve updated the code per Olivers response, it renders correctly using the portal but the child components still don’t rerender on state changes in the Parent Component

const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");

function Child(args) {
  return ReactDOM.createPortal(<>
   <div>child:  {args.number}</div>
   <button onClick={()=>{args.add(args.number+1)}}>Increment base number</button>
  </>, childRoot);
}

export default class App extends React.Component {
  constructor() {
    super();
    this.state = { data: 0, number:0 };
  }
  
 
  add = (val)=> {
    this.setState({
      ...this.state,
      number: val
    });
  }
  addChild = () => {
    this.setState(prevState => ({data: prevState.data + 1}));
  }
  render() {
    const children = Array(this.state.data)
      .fill()
      .map((_, i) => <Child key={i} number={0} add={this.add}/>);
  
    return ( 
      <div>
        <button onClick={this.addChild}>
          add child
        </button>
        <div> data: {this.state.data}</div>
        {children}
      </div>
    );
  }
}

ReactDOM.render(<App/>, root);

Update 2:

The culprit was found. Changed

      number={0}

to

      number={this.state.number}

and it works

Advertisement

Answer

React.createPortal must be used inside the render method (I used a class component because I cannot use hooks in the SO example, you can of course use a functional component).

You can use it in the App component like below or in the Child component :

const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");

function Child({number}) {
  return <div>child {number}</div>;
}

class App extends React.Component {
  constructor() {
    super();
    this.state = { data: 0 };
  }
  
  addChild = () => {
    this.setState(prevState => ({data: prevState.data + 1}));
  }
  
  render() {
    const children = Array(this.state.data)
      .fill()
      .map((_, i) => <Child key={i} number={i} />);
  
    return ( 
      <div>
        <button onClick={this.addChild}>add child</button>
        <div> data: {this.state.data}</div>
        {ReactDOM.createPortal(children, childRoot)}
      </div>
    );
  }
}

ReactDOM.render(<App/>, 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="root"></div>
<div id="childPlaceholder"></div>
Advertisement