I have a button FontAwesomeIcon that, when clicking, opens a modal. However, the issue is that for some reason you need to click the button twice in order for the modal to open. I’ve seen many other posts with similar issues, but it doesn’t seem to make sense to me.
Can someone explain what’s going on here? I know that the setter variable is asynchronous, but even adding async await to the useEffect does nothing. I vaguely remember reading somewhere that you shouldn’t (or can’t) use state setting inside a useEffect but can’t remember why, although the next version of react may change that.
From looking at the console logs, A is always logged last, which makes me believe that const [modalIsOpen, setModalIsOpen] = useState<boolean>(false); is running after the state setting in the useEffect and redeclaring the value to false (although maybe not as even when the modal shows after 2 clicks, it logs A last again). The console logs also suggest that after the first click it sets the state correctly but then something immediately sets the open state back to false, despite there being apparently nothing that should do this. Also might be worth noting that hovering over the button, clicking once, then hovering out and then over the button again requires a further 2 clicks to open the modal.
Here’s the Buttons component:
const Buttons: FC<ButtonsProps> = ({ expanded, setExpanded }) => {
console.log('A');
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
const handleClick = () => {
console.log('B');
setModalIsOpen(true);
// use currentPage.id to determine which item to delete
};
React.useEffect(() => {
console.log(
'change of modalIsOpen or setModalIsOpen detected - now modalIsOpen is:',
modalIsOpen
);
}, [modalIsOpen]);
return (
<div className={styles.buttonsContainer}>
<FontAwesomeIcon
icon={faTrash}
onMouseEnter={() => setDeleteHovered(true)}
onMouseLeave={() => setDeleteHovered(false)}
onClick={() => handleClick()}
/>
<DeletePageModal
modalIsOpen={modalIsOpen}
setModalIsOpen={setModalIsOpen}
/>
</div>
);
};
And the DeletePageModal component:
export const DeletePageModal: FC<DeletePageModalProps> = ({
modalIsOpen,
setModalIsOpen,
}) => {
const afterOpenModal: () => void = () => {
// references are now sync'd and can be accessed.
console.log('Modal opened');
};
const closeModal: () => void = () => {
setModalIsOpen(false);
};
// onRequestClose: do something when clicked close
// contentLabel: just label the modal (aria name for accessibility)
return (
<div>
<Modal
isOpen={modalIsOpen}
onAfterOpen={afterOpenModal}
onRequestClose={closeModal}
className={styles.modal}
contentLabel='Delete page confirmation'>
<button onClick={closeModal} className={styles.close}>
<FontAwesomeIcon icon={faTimes} />
</button>
<div>
<h2>Title</h2>
<p>Content</p>
<button onClick={closeModal} className={styles.button}>
Close
</button>
</div>
</Modal>
</div>
);
};
Thank you
Edit: for more context, I've included more parent containers which show where setDeleteHovered comes from:
export const Sidebar: FC<SidebarProps> = ({ pages, currentPageData }) => {
const [expanded, setExpanded] = useState<boolean>(true);
const [deleteHovered, setDeleteHovered] = useState<boolean>(false);
const { currentPage, fetchPage } = currentPageData;
const SidebarList: FC<{
pages: Array<WebPage>;
deleteHovered: boolean;
}> = ({ pages }) => (
<ul>
{pages.map((page: WebPage) => {
const isCurrentPage = page?.id === currentPage?.id;
const classes = `${isCurrentPage ? styles.current : ''} ${
isCurrentPage && deleteHovered ? styles.deleteHovered : ''
}`;
return (
<li
key={page.title}
className={classes}
onClick={() => {
if (isCurrentPage) return;
fetchPage(`http://localhost:8000/api/pages/${page.id}`);
}}>
<p>{page.title}</p>
<div className={styles.iconContainer}>
<FontAwesomeIcon icon={faTrash} />
</div>
</li>
);
})}
</ul>
);
interface ButtonsProps {
expanded: boolean;
setExpanded: Dispatch<boolean>;
setDeleteHovered: Dispatch<boolean>;
}
const Buttons: FC<ButtonsProps> = ({ expanded, setExpanded }) => {
console.log('A');
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
const handleClick = () => {
console.log('B');
setModalIsOpen(true);
// use currentPage.id to determine which item to delete
// unknown bug SO post: https://stackoverflow.com/questions/70089132/seemingly-simple-react-state-requires-clicking-twice-to-change-the-state-properl
};
React.useEffect(() => {
console.log(
'change of modalIsOpen or setModalIsOpen detected - now modalIsOpen is:',
modalIsOpen
);
}, [modalIsOpen]);
return (
<div className={styles.buttonsContainer}>
<FontAwesomeIcon icon={faPlusSquare} />
<FontAwesomeIcon
icon={faTrash}
className={styles.expandedOnly}
onMouseEnter={() => setDeleteHovered(true)}
onMouseLeave={() => setDeleteHovered(false)}
onClick={() => handleClick()}
/>
<FontAwesomeIcon
icon={expanded ? faCompressArrowsAlt : faExpandArrowsAlt}
onClick={() => setExpanded(!expanded)}
/>
<DeletePageModal
modalIsOpen={modalIsOpen}
setModalIsOpen={setModalIsOpen}
/>
</div>
);
};
const classes = `${styles.sidebar} ${
expanded ? styles.expanded : styles.contracted
}`;
return (
<div className={classes}>
<h1>Pages</h1>
<SidebarList pages={pages} deleteHovered={deleteHovered} />
<Buttons
expanded={expanded}
setExpanded={setExpanded}
setDeleteHovered={setDeleteHovered}
/>
</div>
);
};
setDeleteHoveredcome from, it doesn't look to be declared in your code? This looks like it should work, but if the hover is causing a total unmount-remount, then your state will be lostsetDeleteHoveredcomes from a parent component's state