react ui components
contextcompound-componentsrefs
Popover
Build a popover where the trigger and content are separate components.
What it tests
Sharing state between sibling components without the caller having to wire it.
The consumer should write <Popover><Trigger /><Content /></Popover> and not
manage an isOpen boolean themselves.
The approach
The compound-component pattern. A provider owns the open state and the parts read it from context, so they coordinate no matter how they're nested:
const PopoverContext = createContext(null);
export function usePopoverContext() {
const ctx = useContext(PopoverContext);
if (!ctx) throw new Error("Popover parts must be used inside <Popover>");
return ctx;
}Throwing from the hook when the context is missing turns a confusing null dereference into a message that names the actual mistake.
Where it usually goes wrong
- Prop-drilling
isOpen. Pushes the state back to the caller, which is the thing the component was meant to hide. cloneElementon children. Breaks the moment a part is wrapped in a div.- No outside-click or Escape handling. Both are expected of a popover, and both need a listener that's cleaned up.
- Clipping. A popover inside
overflow: hiddengets cut off; that's what portals are for.
Full solution
1 file from src/folders/popover.
use-popover-context.jsx
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useRef, useState } from "react";
import { createPortal } from "react-dom";
const PopOverContext = createContext(null);
function PopOverProvider({ children }) {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef(null);
function handlePopOver() {
setIsOpen((prev) => !prev);
const { left, right } = contentRef.current?.getBoundingClientRect() ?? {};
console.log(left, right);
}
return (
<PopOverContext.Provider
value={{ isOpen, contentRef, handlePopOver, setIsOpen }}
>
{children}
</PopOverContext.Provider>
);
}
function usePopOverContext() {
const ctxt = useContext(PopOverContext);
if (!ctxt) throw new Error("usePopOverContext used outside PopOverProvider");
return ctxt;
}
function PopOver({ children }) {
return <>{children}</>;
}
function Action({ children }) {
const { handlePopOver } = usePopOverContext();
return <button onClick={handlePopOver}>{children}</button>;
}
function Content({ children }) {
const { isOpen, contentRef } = usePopOverContext();
if (!isOpen) return null;
return createPortal(<div ref={contentRef}>{children}</div>, document.body);
}
PopOver.Action = Action;
PopOver.Content = Content;
export { usePopOverContext, PopOverProvider, PopOver };share this post