Problem Statement
Build a modal that opens from a "Show Modal" button and closes when the user clicks its Close button or clicks outside it.
Functional Requirements
- Open: Clicking "Show Modal" sets
showModaltotrueand shows the modal over a dark overlay. - Close button: The Close button inside the modal calls
closeModal. - Click outside: A
mousedownanywhere outside the modal box closes it. Clicks inside the box do not. - Hidden when closed: When
isOpenisfalse, theModalcomponent renders nothing.
Technical Constraints
- Put the outside click logic in a reusable
useClickOutside(elementRef, handler)hook. - Add the
mousedownlistener ondocumentand remove it in the effect cleanup.
1. Code Flow
The implementation follows this sequence:
- User clicks "Show Modal" button in App component
showModalstate changes totrue- Modal component receives
isOpen={true} - Modal renders and sets up click-outside detection
- When clicking outside or the close button,
showModalreturns tofalse
2. Function Breakdown
App Component
function App() {
const [showModal, setShowModal] = useState(false);
// Controls modal visibility through state
// Initial state is false (modal hidden)
}Modal Component
const Modal = ({ isOpen, closeModal }) => {
// Receives two props:
// isOpen: boolean to control visibility
// closeModal: function to hide modal
// Early return pattern - if not open, render nothing
if (!isOpen) return null;
};useClickOutside Hook
const useClickOutside = (elementRef, handler) => {
// Custom hook that takes:
// elementRef: reference to modal element
// handler: function to call when clicking outside
useEffect(() => {
const cb = (event) => {
// Checks if click was outside modal
// elementRef.current?.contains(event.target) checks if clicked element
// is a child of modal
};
}, [elementRef, handler]);
};3. Understanding useRef
useRef is crucial here for several reasons:
-
DOM Reference:
const modalRef = useRef()creates a mutable reference- This reference stays consistent between re-renders
- Allows direct access to DOM element via
modalRef.current
-
Use in Modal:
<div ref={modalRef} className="modal-container">- Attaches reference to modal's DOM element
- Enables checking if clicks occur inside/outside modal
-
Performance:
- Changes to ref don't trigger re-renders
- Perfect for DOM manipulations and event handling
4. Click vs MouseDown Events
The difference between click and mousedown in this context is crucial:
-
Event Order:
mousedown โ mouseup โ click -
Why mousedown works better:
mousedownfires immediately when mouse button is pressedclickwaits for complete click action (press + release)- Modal can interfere with
clickevent completion
-
Common Issue:
// This might not work reliably document.addEventListener("click", cb); // This works better document.addEventListener("mousedown", cb);
Interview Tips ๐ฏ
-
Key Points to Remember:
- Modals are portals to render outside normal DOM hierarchy
- useRef for DOM manipulation without re-renders
- Event delegation for click-outside detection
- Cleanup in useEffect to prevent memory leaks
-
Common Interview Questions:
- "How would you handle multiple modals?"
- "How would you implement modal accessibility?"
- "What are the advantages of useRef over direct DOM manipulation?"
- "How would you optimize modal performance?"
React Modal Interview Questions & Answers ๐ฏ
1. How would you handle multiple modals?
Solution 1: Using Modal Manager
const [modals, setModals] = useState({
deleteModal: false,
editModal: false,
shareModal: false,
});
const openModal = (modalKey) => {
setModals({ ...modals, [modalKey]: true });
};Solution 2: Modal Stack
const [modalStack, setModalStack] = useState([]);
// Push new modal
const pushModal = (modalType, modalProps) => {
setModalStack([...modalStack, { type: modalType, props: modalProps }]);
};
// Pop modal
const popModal = () => {
setModalStack((prev) => prev.slice(0, -1));
};Key Interview Points ๐
- Discuss Z-index management
- Modal history management
- Proper focus management between modals
- Memory considerations
- Event handling complexity
2. How would you implement modal accessibility?
Core Implementation
const Modal = ({ isOpen, onClose, title }) => {
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex="-1"
>
<h2 id="modal-title">{title}</h2>
{/* content */}
</div>
);
};Key Interview Points ๐
-
ARIA Attributes:
role="dialog"aria-modal="true"aria-labelledbyaria-describedby
-
Keyboard Navigation:
const handleKeyDown = (e) => { if (e.key === "Escape") onClose(); if (e.key === "Tab") handleTabKey(e); }; -
Focus Management:
useEffect(() => { const focusable = modalRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusable.length) focusable[0].focus(); // Store previous focus const previousFocus = document.activeElement; return () => previousFocus.focus(); }, [isOpen]);
3. What are the advantages of useRef over direct DOM manipulation?
Key Interview Points ๐
-
Persistent Reference:
const elementRef = useRef(null); // Survives re-renders without causing them -
React Integration:
// โ Good - React way const inputRef = useRef(); inputRef.current.focus(); // โ Bad - Direct DOM document.getElementById("input").focus(); -
Performance Benefits:
- No re-renders on reference updates
- Clean integration with React lifecycle
- Prevents stale closure issues
-
Memory Management:
- Automatically cleaned up by React
- No manual event listener cleanup needed
4. How would you optimize modal performance?
Code-Level Optimizations
// 1. Lazy Loading
const Modal = React.lazy(() => import("./Modal"));
// 2. Memoization
const MemoizedModal = React.memo(Modal);
// 3. Portal Usage
return createPortal(<Modal />, document.getElementById("modal-root"));Key Interview Points ๐
-
Rendering Optimizations:
- Use
React.memofor modal components - Implement lazy loading for modals
- Use CSS transform instead of display/visibility
- Use
-
State Management:
// Instead of const [isOpen, setIsOpen] = useState(false); // Consider using reducer for complex modals const [state, dispatch] = useReducer(modalReducer, initialState); -
Event Delegation:
// Instead of multiple listeners document.addEventListener("mousedown", handleGlobalClick, { capture: true, });