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>