Teleport react components in your page

Teleport react components in your page

Whenever you need to take content with functionality from a component and put it somewhere else on the page, you can use a react portal.

For example, if you have an input component that outputs some text in a div, you can pass that div to a portal and render it in another component or place on the page.

How portals basically work is as follows:

  • you pass a react element and a DOM element to a function createPortal
  • createPortal attaches the react element to the DOM element
  • you take the DOM element and append it in a place in the DOM

This is useful when you have design constraints that require you to pass data and behavior from a react component to other places in the same page. Note that the source and the target need to be in the same page, meaning that the component that is passing the data should be in the same page as the target that is receiving that data.

Find a detailed video explanation here

For this, you can create a reusable component

import { FunctionComponent, PropsWithChildren, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

interface ComponentProps {
    id: string
}

export const Portal: FunctionComponent<PropsWithChildren<ComponentProps>> = ({ id, children }):JSX.Element => {
  const domElementToAppend = useRef<HTMLDivElement>();
  const [isClientSide, setIsClientSide] = useState(false);

  useEffect(() => {
    const target = document.getElementById(id);
    domElementToAppend.current = document.createElement('div');

    if (!domElementToAppend.current) {
      return;
    }

    target?.appendChild(domElementToAppend.current);

    setIsClientSide(true);

    return () => {
      if (domElementToAppend.current) {
        target?.removeChild(domElementToAppend.current);
      }
    };
  }, []);

  return (
    <>
      {
        (isClientSide && domElementToAppend.current) && createPortal(children, domElementToAppend.current)
      }
    </>
  );
};

You need to create an html element, domElementToAppend. This is a ref element, because you need to initialize it in the useEffect hook as a DOM component, and then, the actual data is passed from the createPortal call in the template.

For server side rendered pages you need to make sure that the createPortal is called only when you are on the client side and after you have initialized domElementToAppend as a DOM component, else you won't see anything in the template at first render.

The target is an actual DOM element that is already in the page. This is the place where you will attach the data from the component that sends data to the portal.

Then you use this in your components like so:

import React, { ChangeEvent, FunctionComponent, useState } from 'react';
import { Portal } from '../shared/Portal';

export const InputToDiv: FunctionComponent<any> = (): JSX.Element => {
  const [text, setText] = useState('');
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  };

  //
  const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
    event.preventDefault();
    console.log('clicked!!!!');
  };

  return (
    <div onClick={handleClick}>
      <input type="text" onChange={handleChange}/>
      <Portal id="my-input-portal">
        <div>
          <strong>Typed text is {text}</strong>
        </div>
      </Portal>
    </div>
  );
};

And then, in the actual page you need both the component and the portal

//...
        <InputToDiv />
//...

<div id="my-input-portal"></div>

You can find the code here