reactjsmaterial-uireact-domreact-portal

React createPortal does not add element to DOM without any errors


I am trying to create a dynamic modal with React and MaterialUI.

Currently adding them like this:

// Confirm.js
import { WarningAmber } from '@mui/icons-material'
import { LoadingButton } from '@mui/lab'
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
import { t } from 'i18next'
import { useState } from 'react'

export default function Confirm({ open, setOpen, title, message, onConfirm, onCancel = () => null }) {
  const [saving, setSaving] = useState(false)

  function handleCancel() {
    onCancel()
    setOpen(false)
  }

  function handleConfirm() {
    setSaving(true)
    onConfirm()
    setSaving(false)
    setOpen(false)
  }

  return (
    <Dialog onClose={() => setOpen(false)} open={open}>
      <DialogTitle className="flex items-center">
        <WarningAmber className="mr-2" />
        {title}
      </DialogTitle>
      <DialogContent>
        <DialogContentText>{message}</DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button className="mx-2" variant="text" onClick={handleCancel}>
          {t('cancel')}
        </Button>
        <LoadingButton loading={saving} variant="contained" onClick={handleConfirm}>
          {t('yes')}
        </LoadingButton>
      </DialogActions>
    </Dialog>
  )
}
// in any component
export default function SomeTable() {
const [confirmDelete, setConfirmDelete] = useState(false)
const [something, setSomething] = useState(null)

// triggered by a button on table
function handleDelete(item) {
setConfirmDelete(true)
setSomething(item)
}
async function deleteSomething() {
// delete
}

return (
<>
{*
table
*}

  <Confirm
    open={confirmDelete}
    setOpen={setConfirmDelete}
    title="are-you-sure"
    message="delete-confirm-message"
    onConfirm={deleteSomething}
  />
</>
);
}

I dont want to add the Confirm component to every other component like this.

Instead I want this:

// useDialog.js
import { useState } from "react";
import { createPortal } from "react-dom";
import Confirm from "../components/common/Confirm";

export default function useConfirm() {
  const [open, setOpen] = useState(true);

  return {
    confirm: ({ title, message, onConfirm, onCancel = () => null }) =>
      createPortal(
        <Confirm
          open={open}
          setOpen={setOpen}
          title={title}
          message={message}
          onConfirm={onConfirm}
          onCancel={onCancel}
        />,
        document.getElementById("root")
      )
  };
}
// any component
import useConfirm from "./hooks/useConfirm";
import "./styles.css";
import { Button } from "@mui/material";

export default function App() {
  const { confirm } = useConfirm();
  return (
    <div className="App">
      <Button
        onClick={() =>
          confirm({
            title: "Modal Title",
            message: "Modal Message",
            onConfirm: () => null
          })
        }
      >
        Open Confirm Modal
      </Button>
    </div>
  );
}

But when I do it like this, createPortal is not doing anything, not throwing any errors either. Here is the sandbox https://codesandbox.io/s/modest-saha-hjvgwz?file=/src/App.js


Solution

  • Followed @ArsanyBenyamine's comment and got it to working by converting it to context. I added Confirm component globally and controlled its state in in the context. Here is the final sandbox https://codesandbox.io/p/devbox/modest-saha-hjvgwz