0

I am attempting to make a simple Portal wrapper that does the following.

  1. Can render multiple portals on the same view
  2. Can render multiple portals by targeting parent ids. This cannot be done by passing a ref into the component since the container may live anywhere.
  3. Can render multiple portal without any parent targets

This is what I came up with this morning. The code works exactly how I want it to work. It creates portals with or without a parent target and cleans up completely. However React says not to call hooks from conditions, which you can see I have done here by calling useRef within a ternary. I have not seen any warnings or errors but I am also developing this code in a custom setup with Webpack and Typescript. I have not pushed this code to NPM to see what happens when I import the library into a project. My guess is there's going to be issues. Is there a better way to do this?

Thanks in advance.

Calling Component:

<div>
  {open1 && (
    <Portal parentId="panel-1">
      <Panel title="Poopster" onClose={handleClose1}>
        <div>Panel</div>
      </Panel>
    </Portal>
  )}
  {open2 && (
    <Portal>
      <Panel onClose={handleClose2}>
        <div>Panel</div>
      </Panel>
    </Portal>
  )}
<div>

Portal Component

import * as React from 'react';
import { createPortal } from 'react-dom';

export interface PortalProps {
  children: React.ReactElement;
  parentId?: string;
}

export const Portal = ({
  children,
  parentId
}: PortalProps): React.ReactElement => {

  let ref = !parentId ?
    React.useRef(document.createElement('div')) :
    React.useRef(document.getElementById(parentId));

  React.useEffect((): VoidFunction => {
    return (): void => {
      if (!parentId && ref.current) {
        document.body.removeChild(ref.current);
      }

      ref = null;
    };
  }, []);

  React.useEffect(() => {
    if (!parentId && ref.current) {
      document.body.appendChild(ref.current);
    }
  }, [ref, parentId]);

  return createPortal(children, ref.current);
};

export default Portal;
Aaron
  • 2,364
  • 2
  • 31
  • 56
  • I think this would actually be one of the few situations where it'd be a good idea to hold a reference to a DOM element in state. – AKX Sep 09 '21 at 13:57
  • I think with React, you are just putting the initial value inside the parens, not its commitment to a permanent value. The ref's `current` property is mutable and can be reassigned. Maybe something like `const parentRef = React.useRef(null);` and `parentRef.current = parentId ? foo : bar;` – Kevin Ashworth Sep 09 '21 at 13:58

1 Answers1

0

You could make it like so, for short and more intuitive

let ref = React.useRef(
  parentId ? document.getElementById(parentId) : document.createElement("div")
);

This will solve the hook rules error

Ryan Le
  • 7,708
  • 1
  • 13
  • 23