DEV Community

Cover image for React: Building an Independent Modal with createRoot
SeongKuk Han
SeongKuk Han

Posted on

React: Building an Independent Modal with createRoot

Back in 2021, when I started React for the first time in my career. I managed modal components by using conditional rendering.

const Component = () => {
  // ...

  return (
    <div>
      {visible && <Modal onOk={handleOk} />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is based on the idea that a component is rendered when it has to.

In another way, modal components decide whether it should be rendered or not by the logic written inside the modal component.

const Component = () => {
  // ...

  return (
    <div>
      <Modal visible={visible} onOk={handleOk} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we don't need to do conditional rendering, but passing a prop to render it.

As I dived into React more, I used a context provider to manage modal components so that I can simply use hooks to render modals.

const Component = () => {
  const {showModal} = useModal();

  const handleModalOpen = () => {
    showModal({
      message: 'hello',
      onConfirm: () => { alert('clicked'); }
    });

  }

  return (
    <div>
        {/*...*/}
    </div>
  );
}

const ModalProvider = () => {
  // ...
  return (
    <ModalContext.Provider value={{
      showModal,
    }}>
      <Modal {...modalState} />
    </ModalContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

I even wrote a post about this management, here.

In the side project I recently started, I had to create modal components.

I didn't want to write code from different places, I just wanted to call a function and render a modal component, and an idea came to my mind – render a component on a new root.

In this way, we don't write extra code to render a modal somewhere like in the root or wherever. Modal rendering logic is done inside the component.

The implementation could be different depending on your project.

In my project, I wrote the modal component like this:

import { useCallback, useState } from 'react';
import Text from '../../Text';
import Button from '../../Button';
import { createRoot } from 'react-dom/client';
import Input from '../../Input';

type ConfirmModalProps = {
  title: string;
  titleColor: 'red';
  message: string;
  confirmText: string;
  confirmationPhrase?: string;
  cancelText?: string;
  onConfirm: VoidFunction;
  onCancel: VoidFunction;
};

type ConfirmParameters = Omit<ConfirmModalProps, 'onConfirm' | 'onCancel'> & {
  onConfirm?: VoidFunction;
  onCancel?: VoidFunction;
};

const ConfirmModal = ({
  title,
  titleColor,
  message,
  confirmText,
  confirmationPhrase,
  cancelText = 'Cancel',
  onConfirm,
  onCancel,
}: ConfirmModalProps) => {
  const [input, setInput] = useState('');

  const confirmButtonDisabled =
    confirmationPhrase !== undefined && input !== confirmationPhrase;

  return (
    <div className="absolute z-50 inset-0 bg-black/50">
      <div className="fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] rounded-lg p-6 shadow-lg w-full max-w-[calc(100%-2rem)] sm:max-w-lg bg-gray-900 border border-gray-700 flex flex-col gap-2">
        <Text size="lg" color={titleColor} className="font-bold">
          {title}
        </Text>
        <Text>{message}</Text>
        {confirmationPhrase ? (
          <Input
            className="w-full"
            placeholder={`${confirmationPhrase}`}
            onChange={(e) => setInput(e.target.value)}
          />
        ) : null}
        <div className="flex justify-end gap-2 mt-2">
          <Button color="lightGray" onClick={onCancel}>
            {cancelText}
          </Button>
          <Button
            color="red"
            varient="fill"
            onClick={onConfirm}
            disabled={confirmButtonDisabled}
          >
            {confirmText}
          </Button>
        </div>
      </div>
    </div>
  );
};

export const useConfirmModal = () => {
  const confirm = useCallback(
    ({ onConfirm, onCancel, ...params }: ConfirmParameters) => {
      const tempElmt = document.createElement('div');
      document.body.append(tempElmt);
      const root = createRoot(tempElmt);

      const handleConfirm = () => {
        tempElmt.remove();
        onConfirm?.();
      };

      const handleCancel = () => {
        tempElmt.remove();
        onCancel?.();
      };

      root.render(
        <ConfirmModal
          {...params}
          onConfirm={handleConfirm}
          onCancel={handleCancel}
        />
      );
    },
    []
  );

  return {
    confirm,
  };
};
Enter fullscreen mode Exit fullscreen mode

Modal

The main logic is here:

export const useConfirmModal = () => {
  const confirm = useCallback(
    ({ onConfirm, onCancel, ...params }: ConfirmParameters) => {
      const tempElmt = document.createElement('div');
      document.body.append(tempElmt);
      const root = createRoot(tempElmt);

      // ...

      root.render(
        <ConfirmModal
          {...params}
          onConfirm={handleConfirm}
          onCancel={handleCancel}
        />
      );
    },
    []
  );

  return {
    confirm,
  };
};
Enter fullscreen mode Exit fullscreen mode

It adds the modal component in the body. There is no other code needed. I don't need to write code somewhere else

What I need to do is just calling the function:

  const { confirm } = useConfirmModal();

  const handleDeleteClick = () => {
    confirm({
      title: 'Are you absolutely sure?',
      message:
        'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
      titleColor: 'red',
      confirmationPhrase: 'delete my account',
      confirmText: 'Yes, delete my account',
    });
  };
Enter fullscreen mode Exit fullscreen mode

This modal component itself is independent.

It might not be a good fit on your project though, I wanted to introduce a way to manage modal components in react in this post.

I hope you found it helpful.

Happy Coding!

Top comments (0)