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} />
);
}