Bubunyo Nyavor
Bubunyo Nyavor

Reputation: 2570

Render Component inside a different location

This is my react app component structure

<App>
    <Body>
        <Top>
            ### //<- Position 1
        </Top>
        <Middle>
            <MyComponent/>
        </Middle>
    </Body>
</App>

I want the <MyComponent/> to render at Position 1 anytime it is imported. I have tried ReactDom.createPortal but i keep getting Target container is not a DOM element. If this is not possible how best can i achieve a similar setup.

What I have tried

//Top.js
<Top>
    <div id="top-render"/>
</Top>

//MyCustomElement.js
class MyCustomElement extends Component{
    render(){
        return (<div>Demo Text</div>)
    }
}

export default props => ReactDOM.createPortal(
    <MyCustomElement />,
    document.getElementById('top-render'),
);

//Middle.js
<Middle><MyCustomElement/></Middle>

Upvotes: 2

Views: 1940

Answers (3)

Nana Ewusi
Nana Ewusi

Reputation: 66

You are getting the Target container is not a DOM element error, because by the time execution reaches your ReactDOM.createPortal() call, the DOM node that document.getElementById('top-render') is attempting to retrieve, hasn't yet been written to the DOM.

This is because by that time, React is yet to make a successful pass through your component tree. i.e. React is yet to successfully create a virtual representation of everything in the component tree, and as a result, nothing has been written to DOM yet. So although you were expecting the top-render div to be there in the DOM, it hadn't actually been written yet.

The solution is to create a DOM node owned by <MyComponent, that will contain it's children (let's call this node el), then pass that DOM node, as the target DOM element in your React.createPortal() call (aka, it's second argument). Then when <MyComponent> mounts, you simply retrieve the top-render div and append el to it. Like so:

const MyCustomComponent = () => {
  return (<div>Demo Text: from "MyCustomComponent"</div>)
}

class MyComponent extends React.Component {
  el = document.createElement("div");

  componentDidMount() {
    // saving this on the component so we can access it in `componentWillUnmount` later
    this.targetEl = document.getElementById("top-render");
    this.targetEl.appendChild(this.el);
  }

  componentWillUnmount() {
    this.targetEl.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(<MyCustomComponent />, this.el);
  }
}

I put together a working example.

This approach works, because React guarantees that componentDidMount() will get called immediately after the component is mounted (aka. written to the DOM). In other words, we know for sure that by the time componentDidMount() gets called, our component tree has been written to the DOM at least once, and consequently, the top-render div exists in the DOM.

Upvotes: 4

Sajal Preet Singh
Sajal Preet Singh

Reputation: 379

ReactDom.createPortal is suggested to be used on DOM nodes outside the scope of React App. Inside you should declare a component normally <Top><MyComponent/></Top>.

But for curiosity's sake, in case you want to go with this approach only, you should use the ref of the DOM node in Top element.

ReactDOM.createPortal(
    <MyComponent/>,
    /*Ref of Container element*/,
);

Upvotes: 0

Hamed Hamedi
Hamed Hamedi

Reputation: 1546

What do you mean by "Anytime it is imported" ?

Anyway you can just use conditional rendering. It will do the job, but it seems your intention is something else!

render(){
 const { showMyComp } = this.state
 return(
   <App>
    <Body>
        <Top>
            {showMyComp && (<MyComponent />)}
        </Top>
        <Middle>
            <MyComponent/>
        </Middle>
    </Body>
   </App>
 )
}

Upvotes: 0

Related Questions