How to make a Modal Component manage its own state in Next.js/React when I can't reference it?

I’m trying to code a drop-in solution for showing error messages. I want the modal to manage its own state (what text to display, whether the modal is showing at all) instead of the page in which the modal appears.

However, trying to reference the Modal component results in an error saying that I can’t do that.

Here’s an abridged version of the code that I was trying to get working.

import { useRef } from 'react'
import Layout from 'src/client/components/layout'
import Modal from 'src/client/components/modal'

export default function Page() {

    const modalRef = useRef<typeof Modal>()
    const showAlert = (body: string) => {modalRef.current?.showModal(body, 'Error', 'danger');}
/*                                       ^~~~~~~~~~~~~~~~
 * ends up being undefined */

    const submitForm = async () => {
        try {
            doThing()
        except (e) {
            showAlert(e.message)
        }
    }

    return (
        <Layout>
            <Content />{/* <-- the specifics don't really matter here */}
            <Modal ref={modalRef} />
{/*                ^~~~~~~~~~~~~~
 * IntelliSense says:
 * Type '{ ref: MutableRefObject<(() => Element) | undefined>; }' is not assignable to type 'IntrinsicAttributes'.
 * Property 'ref' does not exist on type 'IntrinsicAttributes'.ts(2322)" */}
        </Layout>
    )
}

My current implementation of the modal requires the parent to manage the modal’s state using 4 useState() calls and to change those inside the parent by writing a function for that. This is not scalable and goes against the very point of having a Component in the first place.

// counter-example
import { useState } from 'react'
...
export default function Page(){
    // Yuck! Unmaintainable code!
    const [modalVisible, setModalVisible] = useState(false)
    const [modalBodyText, setModalBodyText] = useState('')
    const [modalTitleText, setModalTitleText] = useState('')
    const [modalType, setModalType] = useState('')
...
    return (
        <Layout>
            <Content />
            {/* Yuck! Unmaintainable code! */}
            <Modal
                body={bodyText}
                visible={modalVisible}
                title={modalTitle}
                type={modalType} />
        </Layout>
    );

I come from a background of HTML and Godot Engine where this kind of stuff is second nature. How can this pattern be implemented in React if a Component can’t have a reference from which to call its intrinsic functions?

Here’s the implementation of the Modal that I want to use:

import { forwardRef, useState } from 'react'

export default function Modal() {
    const [bodyText, setBodyText] = useState<string>('')
    const [titleText, setTitleText] = useState<string>('')
    const [type, setType] = useState<string>('')
    const [visible, setVisible] = useState<boolean>(false)

    function showModal(body, type, title) {
        setBodyText(body)
        setTitleText(title)
        setType(type)
        setVisible(true)
    }

    function onClose() {setVisible(false)}

    // styles omitted:
    return visible ? (
        <div className="m_bg">
            <div className="m_box">
                <button
                    type='button'
                    onClick={onClose}
                    >×</button>
                <div className="m_body">
                    <img src={
                        type == 'danger'
                            ? '/images/icon_error.svg'
                            : '/images/icon_info.svg'}
                    ></img>
                    <div>
                        <div>
                            {titleText}
                        </div>
                        <div>
                            {bodyText}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    ) : (
        <div></div>
    )
}

>Solution :

To make it work correctly you need to wrap the Modal component with forwardRef function and move the showAlert function to useImperativeHandle function so it is accessible from the parent component.

import React from 'react';

const Modal = React.forwardRef((_, ref) => {
  const showModal = (value: string) => {};

  React.useImperativeHandle(ref, () => ({
    showModal: (value: string) => {
      showModal(value);
    }
  }));

  return null;
});

export type ModalHandleProps = {
  showModal: (body: string) => void;
};

const App = () => {
  const modalRef = React.useRef<ModalHandleProps>();
  const showAlert = (body: string) => {modalRef.current?.showModal(body);}

  return (
    <Modal ref={modalRef} />
  );
}

Playground

Leave a Reply