[{"data":1,"prerenderedAt":1686},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-portals-refs-guide":3},{"id":4,"title":5,"body":6,"description":1671,"difficulty":1672,"extension":1673,"framework":1674,"frameworkSlug":1675,"meta":1676,"navigation":84,"order":113,"path":1677,"qaPath":1678,"seo":1679,"stem":1680,"subtopic":1681,"topic":1682,"topicSlug":1683,"updated":1684,"__hash__":1685},"blog\u002Fblog\u002Freact-portals-refs-guide.md","React Portals & Refs — Complete Interview Guide",{"type":7,"value":8,"toc":1654},"minimark",[9,14,18,26,30,35,50,249,253,286,296,303,307,313,424,431,435,438,484,748,755,759,763,781,790,891,897,1053,1057,1121,1128,1132,1141,1280,1287,1291,1301,1373,1387,1526,1538,1542,1545,1575,1582,1592,1596,1650],[10,11,13],"h2",{"id":12},"why-portals-and-refs-come-up-in-senior-react-interviews","Why portals and refs come up in senior React interviews",[15,16,17],"p",{},"React's declarative model handles 95% of UI work beautifully. Portals and refs exist for\nthe other 5% — the cases where you genuinely need to step outside React's rendering model\nand talk to the DOM directly. Interviewers ask about them because they reveal whether a\ncandidate understands React's architecture at a deep level, not just how to write JSX.",[15,19,20,21,25],{},"Questions range from \"what does ",[22,23,24],"code",{},"createPortal"," do?\" to subtle gotchas like \"how do events\nbubble through a portal?\" to design-sense questions like \"when do refs indicate a code\nsmell?\" This guide covers all of them.",[10,27,29],{"id":28},"react-portals-the-dom-escape-hatch","React portals — the DOM escape hatch",[31,32,34],"h3",{"id":33},"what-createportal-does","What createPortal does",[15,36,37,40,41,44,45,49],{},[22,38,39],{},"ReactDOM.createPortal(children, domNode)"," renders ",[22,42,43],{},"children"," into a DOM node of your\nchoosing — typically somewhere outside the main app root — while keeping those children\nfully inside the ",[46,47,48],"strong",{},"React component tree",". Context, state, refs, and synthetic events all\nflow through the React ancestry unchanged. Only the DOM placement is different.",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-jsx shiki shiki-themes github-light github-dark","import { createPortal } from 'react-dom'\n\nfunction Modal({ children, onClose }) {\n  return createPortal(\n    \u003Cdiv className=\"modal-overlay\" onClick={onClose}>\n      \u003Cdiv className=\"modal-content\" onClick={e => e.stopPropagation()}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>,\n    document.getElementById('modal-root') \u002F\u002F outside #root in the real DOM\n  )\n}\n","jsx","",[22,58,59,79,86,111,123,150,187,193,204,215,237,243],{"__ignoreMap":56},[60,61,64,68,72,75],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"szBVR","import",[60,69,71],{"class":70},"sVt8B"," { createPortal } ",[60,73,74],{"class":66},"from",[60,76,78],{"class":77},"sZZnC"," 'react-dom'\n",[60,80,82],{"class":62,"line":81},2,[60,83,85],{"emptyLinePlaceholder":84},true,"\n",[60,87,89,92,96,99,102,105,108],{"class":62,"line":88},3,[60,90,91],{"class":66},"function",[60,93,95],{"class":94},"sScJk"," Modal",[60,97,98],{"class":70},"({ ",[60,100,43],{"class":101},"s4XuR",[60,103,104],{"class":70},", ",[60,106,107],{"class":101},"onClose",[60,109,110],{"class":70}," }) {\n",[60,112,114,117,120],{"class":62,"line":113},4,[60,115,116],{"class":66},"  return",[60,118,119],{"class":94}," createPortal",[60,121,122],{"class":70},"(\n",[60,124,126,129,133,136,139,142,145,147],{"class":62,"line":125},5,[60,127,128],{"class":70},"    \u003C",[60,130,132],{"class":131},"s9eBZ","div",[60,134,135],{"class":94}," className",[60,137,138],{"class":66},"=",[60,140,141],{"class":77},"\"modal-overlay\"",[60,143,144],{"class":94}," onClick",[60,146,138],{"class":66},[60,148,149],{"class":70},"{onClose}>\n",[60,151,153,156,158,160,162,165,167,169,172,175,178,181,184],{"class":62,"line":152},6,[60,154,155],{"class":70},"      \u003C",[60,157,132],{"class":131},[60,159,135],{"class":94},[60,161,138],{"class":66},[60,163,164],{"class":77},"\"modal-content\"",[60,166,144],{"class":94},[60,168,138],{"class":66},[60,170,171],{"class":70},"{",[60,173,174],{"class":101},"e",[60,176,177],{"class":66}," =>",[60,179,180],{"class":70}," e.",[60,182,183],{"class":94},"stopPropagation",[60,185,186],{"class":70},"()}>\n",[60,188,190],{"class":62,"line":189},7,[60,191,192],{"class":70},"        {children}\n",[60,194,196,199,201],{"class":62,"line":195},8,[60,197,198],{"class":70},"      \u003C\u002F",[60,200,132],{"class":131},[60,202,203],{"class":70},">\n",[60,205,207,210,212],{"class":62,"line":206},9,[60,208,209],{"class":70},"    \u003C\u002F",[60,211,132],{"class":131},[60,213,214],{"class":70},">,\n",[60,216,218,221,224,227,230,233],{"class":62,"line":217},10,[60,219,220],{"class":70},"    document.",[60,222,223],{"class":94},"getElementById",[60,225,226],{"class":70},"(",[60,228,229],{"class":77},"'modal-root'",[60,231,232],{"class":70},") ",[60,234,236],{"class":235},"sJ8bj","\u002F\u002F outside #root in the real DOM\n",[60,238,240],{"class":62,"line":239},11,[60,241,242],{"class":70},"  )\n",[60,244,246],{"class":62,"line":245},12,[60,247,248],{"class":70},"}\n",[31,250,252],{"id":251},"why-you-need-portals-the-z-index-trap","Why you need portals: the z-index trap",[15,254,255,256,259,260,104,263,266,267,270,271,274,275,277,278,281,282,285],{},"The canonical reason to use a portal is to escape a ",[46,257,258],{},"CSS stacking context",". When an\nancestor element has ",[22,261,262],{},"transform",[22,264,265],{},"opacity \u003C 1",", or ",[22,268,269],{},"position"," + ",[22,272,273],{},"z-index",", it creates a\nnew stacking context. Every descendant's ",[22,276,273],{}," is relative to that context, not to\nthe page root. A modal with ",[22,279,280],{},"z-index: 9999"," can still be covered by a sibling stacking\ncontext with ",[22,283,284],{},"z-index: 2",".",[15,287,288,289,292,293,295],{},"Portaling the modal to ",[22,290,291],{},"document.body"," removes it from all ancestor stacking contexts,\nletting it float above everything else without fighting ",[22,294,273],{}," wars.",[15,297,298,299,302],{},"The same logic applies to ",[22,300,301],{},"overflow: hidden"," parents clipping tooltips and dropdown\nmenus. The fix is always the same: portal the overlay to a node outside the clipping\nancestor.",[31,304,306],{"id":305},"the-event-bubbling-gotcha-the-1-interview-trick","The event bubbling gotcha — the #1 interview trick",[15,308,309,310,312],{},"This is the question most candidates get wrong. Events inside a portal bubble through\nthe ",[46,311,48],{},", not the actual DOM tree.",[51,314,316],{"className":53,"code":315,"language":55,"meta":56,"style":56},"function Page() {\n  \u002F\u002F This onClick fires when the button inside the portal is clicked.\n  \u002F\u002F The portal DOM is outside #root, but its React parent is Page.\n  return (\n    \u003Cdiv onClick={() => console.log('caught in Page')}>\n      \u003CModal>\n        \u003Cbutton>Click me\u003C\u002Fbutton>\n      \u003C\u002FModal>\n    \u003C\u002Fdiv>\n  )\n}\n",[22,317,318,328,333,338,345,375,385,400,408,416,420],{"__ignoreMap":56},[60,319,320,322,325],{"class":62,"line":63},[60,321,91],{"class":66},[60,323,324],{"class":94}," Page",[60,326,327],{"class":70},"() {\n",[60,329,330],{"class":62,"line":81},[60,331,332],{"class":235},"  \u002F\u002F This onClick fires when the button inside the portal is clicked.\n",[60,334,335],{"class":62,"line":88},[60,336,337],{"class":235},"  \u002F\u002F The portal DOM is outside #root, but its React parent is Page.\n",[60,339,340,342],{"class":62,"line":113},[60,341,116],{"class":66},[60,343,344],{"class":70}," (\n",[60,346,347,349,351,353,355,358,361,364,367,369,372],{"class":62,"line":125},[60,348,128],{"class":70},[60,350,132],{"class":131},[60,352,144],{"class":94},[60,354,138],{"class":66},[60,356,357],{"class":70},"{() ",[60,359,360],{"class":66},"=>",[60,362,363],{"class":70}," console.",[60,365,366],{"class":94},"log",[60,368,226],{"class":70},[60,370,371],{"class":77},"'caught in Page'",[60,373,374],{"class":70},")}>\n",[60,376,377,379,383],{"class":62,"line":152},[60,378,155],{"class":70},[60,380,382],{"class":381},"sj4cs","Modal",[60,384,203],{"class":70},[60,386,387,390,393,396,398],{"class":62,"line":189},[60,388,389],{"class":70},"        \u003C",[60,391,392],{"class":131},"button",[60,394,395],{"class":70},">Click me\u003C\u002F",[60,397,392],{"class":131},[60,399,203],{"class":70},[60,401,402,404,406],{"class":62,"line":195},[60,403,198],{"class":70},[60,405,382],{"class":381},[60,407,203],{"class":70},[60,409,410,412,414],{"class":62,"line":206},[60,411,209],{"class":70},[60,413,132],{"class":131},[60,415,203],{"class":70},[60,417,418],{"class":62,"line":217},[60,419,242],{"class":70},[60,421,422],{"class":62,"line":239},[60,423,248],{"class":70},[15,425,426,427,430],{},"React's synthetic event system delegates to the React root, not to DOM nodes, so it\nfollows the React ancestry. This is almost always what you want — the component that\nrenders the portal should be able to catch events from it. But it surprises developers\nwho expect events to bubble through the DOM, especially when mixing React synthetic events\nwith native ",[22,428,429],{},"addEventListener"," calls.",[31,432,434],{"id":433},"accessibility-in-portal-modals","Accessibility in portal modals",[15,436,437],{},"A portal handles the DOM placement but not the accessibility. You must add these yourself:",[439,440,441,452,463,470],"ul",{},[442,443,444,447,448,451],"li",{},[22,445,446],{},"role=\"dialog\""," and ",[22,449,450],{},"aria-modal=\"true\""," so screen readers treat it as a dialog",[442,453,454,455,458,459,462],{},"Move focus into the modal when it opens (",[22,456,457],{},"ref.current.focus()"," on the dialog container\nwith ",[22,460,461],{},"tabIndex={-1}",")",[442,464,465,466,469],{},"Trap focus inside the modal while it's open (the ",[22,467,468],{},"focus-trap-react"," library automates this)",[442,471,472,473,476,477,480,481],{},"Close on ",[22,474,475],{},"Escape"," key via a ",[22,478,479],{},"document.addEventListener('keydown', ...)"," in a ",[22,482,483],{},"useEffect",[51,485,487],{"className":53,"code":486,"language":55,"meta":56,"style":56},"const Modal = React.forwardRef(({ isOpen, onClose, children }, ref) => {\n  useEffect(() => {\n    if (!isOpen) return\n    const handler = (e) => e.key === 'Escape' && onClose()\n    document.addEventListener('keydown', handler)\n    return () => document.removeEventListener('keydown', handler)\n  }, [isOpen, onClose])\n\n  if (!isOpen) return null\n\n  return createPortal(\n    \u003Cdiv role=\"dialog\" aria-modal=\"true\" tabIndex={-1} ref={ref}>\n      {children}\n    \u003C\u002Fdiv>,\n    document.body\n  )\n})\n",[22,488,489,532,544,561,597,611,633,638,642,659,663,671,716,722,731,737,742],{"__ignoreMap":56},[60,490,491,494,496,499,502,505,508,511,513,515,517,519,522,525,527,529],{"class":62,"line":63},[60,492,493],{"class":66},"const",[60,495,95],{"class":381},[60,497,498],{"class":66}," =",[60,500,501],{"class":70}," React.",[60,503,504],{"class":94},"forwardRef",[60,506,507],{"class":70},"(({ ",[60,509,510],{"class":101},"isOpen",[60,512,104],{"class":70},[60,514,107],{"class":101},[60,516,104],{"class":70},[60,518,43],{"class":101},[60,520,521],{"class":70}," }, ",[60,523,524],{"class":101},"ref",[60,526,232],{"class":70},[60,528,360],{"class":66},[60,530,531],{"class":70}," {\n",[60,533,534,537,540,542],{"class":62,"line":81},[60,535,536],{"class":94},"  useEffect",[60,538,539],{"class":70},"(() ",[60,541,360],{"class":66},[60,543,531],{"class":70},[60,545,546,549,552,555,558],{"class":62,"line":88},[60,547,548],{"class":66},"    if",[60,550,551],{"class":70}," (",[60,553,554],{"class":66},"!",[60,556,557],{"class":70},"isOpen) ",[60,559,560],{"class":66},"return\n",[60,562,563,566,569,571,573,575,577,579,582,585,588,591,594],{"class":62,"line":113},[60,564,565],{"class":66},"    const",[60,567,568],{"class":94}," handler",[60,570,498],{"class":66},[60,572,551],{"class":70},[60,574,174],{"class":101},[60,576,232],{"class":70},[60,578,360],{"class":66},[60,580,581],{"class":70}," e.key ",[60,583,584],{"class":66},"===",[60,586,587],{"class":77}," 'Escape'",[60,589,590],{"class":66}," &&",[60,592,593],{"class":94}," onClose",[60,595,596],{"class":70},"()\n",[60,598,599,601,603,605,608],{"class":62,"line":125},[60,600,220],{"class":70},[60,602,429],{"class":94},[60,604,226],{"class":70},[60,606,607],{"class":77},"'keydown'",[60,609,610],{"class":70},", handler)\n",[60,612,613,616,619,621,624,627,629,631],{"class":62,"line":152},[60,614,615],{"class":66},"    return",[60,617,618],{"class":70}," () ",[60,620,360],{"class":66},[60,622,623],{"class":70}," document.",[60,625,626],{"class":94},"removeEventListener",[60,628,226],{"class":70},[60,630,607],{"class":77},[60,632,610],{"class":70},[60,634,635],{"class":62,"line":189},[60,636,637],{"class":70},"  }, [isOpen, onClose])\n",[60,639,640],{"class":62,"line":195},[60,641,85],{"emptyLinePlaceholder":84},[60,643,644,647,649,651,653,656],{"class":62,"line":206},[60,645,646],{"class":66},"  if",[60,648,551],{"class":70},[60,650,554],{"class":66},[60,652,557],{"class":70},[60,654,655],{"class":66},"return",[60,657,658],{"class":381}," null\n",[60,660,661],{"class":62,"line":217},[60,662,85],{"emptyLinePlaceholder":84},[60,664,665,667,669],{"class":62,"line":239},[60,666,116],{"class":66},[60,668,119],{"class":94},[60,670,122],{"class":70},[60,672,673,675,677,680,682,685,688,690,693,696,698,700,703,706,709,711,713],{"class":62,"line":245},[60,674,128],{"class":70},[60,676,132],{"class":131},[60,678,679],{"class":94}," role",[60,681,138],{"class":66},[60,683,684],{"class":77},"\"dialog\"",[60,686,687],{"class":94}," aria-modal",[60,689,138],{"class":66},[60,691,692],{"class":77},"\"true\"",[60,694,695],{"class":94}," tabIndex",[60,697,138],{"class":66},[60,699,171],{"class":70},[60,701,702],{"class":66},"-",[60,704,705],{"class":381},"1",[60,707,708],{"class":70},"} ",[60,710,524],{"class":94},[60,712,138],{"class":66},[60,714,715],{"class":70},"{ref}>\n",[60,717,719],{"class":62,"line":718},13,[60,720,721],{"class":70},"      {children}\n",[60,723,725,727,729],{"class":62,"line":724},14,[60,726,209],{"class":70},[60,728,132],{"class":131},[60,730,214],{"class":70},[60,732,734],{"class":62,"line":733},15,[60,735,736],{"class":70},"    document.body\n",[60,738,740],{"class":62,"line":739},16,[60,741,242],{"class":70},[60,743,745],{"class":62,"line":744},17,[60,746,747],{"class":70},"})\n",[15,749,750,751,754],{},"React automatically removes portal DOM when the component unmounts. If you created the\nhost container element yourself (via ",[22,752,753],{},"document.createElement","), you're responsible for\nremoving that container in cleanup.",[10,756,758],{"id":757},"refs-mutable-values-that-dont-drive-renders","Refs — mutable values that don't drive renders",[31,760,762],{"id":761},"the-two-jobs-of-useref","The two jobs of useRef",[15,764,765,768,769,772,773,776,777,780],{},[22,766,767],{},"useRef(initialValue)"," returns ",[22,770,771],{},"{ current: initialValue }"," — a plain object that persists\nacross renders. Changing ",[22,774,775],{},".current"," does ",[46,778,779],{},"not"," schedule a re-render, which is exactly\nthe point.",[15,782,783,786,787,789],{},[46,784,785],{},"Job 1 — DOM access:"," attach ",[22,788,524],{}," to a JSX element to get the underlying DOM node.",[51,791,793],{"className":53,"code":792,"language":55,"meta":56,"style":56},"function TextInput() {\n  const inputRef = useRef(null)\n  return (\n    \u003C>\n      \u003Cinput ref={inputRef} \u002F>\n      \u003Cbutton onClick={() => inputRef.current.focus()}>Focus\u003C\u002Fbutton>\n    \u003C\u002F>\n  )\n}\n",[22,794,795,804,825,831,836,851,878,883,887],{"__ignoreMap":56},[60,796,797,799,802],{"class":62,"line":63},[60,798,91],{"class":66},[60,800,801],{"class":94}," TextInput",[60,803,327],{"class":70},[60,805,806,809,812,814,817,819,822],{"class":62,"line":81},[60,807,808],{"class":66},"  const",[60,810,811],{"class":381}," inputRef",[60,813,498],{"class":66},[60,815,816],{"class":94}," useRef",[60,818,226],{"class":70},[60,820,821],{"class":381},"null",[60,823,824],{"class":70},")\n",[60,826,827,829],{"class":62,"line":88},[60,828,116],{"class":66},[60,830,344],{"class":70},[60,832,833],{"class":62,"line":113},[60,834,835],{"class":70},"    \u003C>\n",[60,837,838,840,843,846,848],{"class":62,"line":125},[60,839,155],{"class":70},[60,841,842],{"class":131},"input",[60,844,845],{"class":94}," ref",[60,847,138],{"class":66},[60,849,850],{"class":70},"{inputRef} \u002F>\n",[60,852,853,855,857,859,861,863,865,868,871,874,876],{"class":62,"line":152},[60,854,155],{"class":70},[60,856,392],{"class":131},[60,858,144],{"class":94},[60,860,138],{"class":66},[60,862,357],{"class":70},[60,864,360],{"class":66},[60,866,867],{"class":70}," inputRef.current.",[60,869,870],{"class":94},"focus",[60,872,873],{"class":70},"()}>Focus\u003C\u002F",[60,875,392],{"class":131},[60,877,203],{"class":70},[60,879,880],{"class":62,"line":189},[60,881,882],{"class":70},"    \u003C\u002F>\n",[60,884,885],{"class":62,"line":195},[60,886,242],{"class":70},[60,888,889],{"class":62,"line":206},[60,890,248],{"class":70},[15,892,893,896],{},[46,894,895],{},"Job 2 — mutable container:"," store any value that needs to survive re-renders without\ncausing them — timer IDs, animation frame IDs, WebSocket instances, previous prop values.",[51,898,900],{"className":53,"code":899,"language":55,"meta":56,"style":56},"function Debounced({ onSearch }) {\n  const timerRef = useRef(null)\n\n  const handleChange = (e) => {\n    clearTimeout(timerRef.current)          \u002F\u002F cancel previous — no re-render\n    timerRef.current = setTimeout(() => {\n      onSearch(e.target.value)\n    }, 300)\n  }\n\n  useEffect(() => () => clearTimeout(timerRef.current), [])\n\n  return \u003Cinput onChange={handleChange} \u002F>\n}\n",[22,901,902,916,933,937,956,967,983,991,1001,1006,1010,1028,1032,1049],{"__ignoreMap":56},[60,903,904,906,909,911,914],{"class":62,"line":63},[60,905,91],{"class":66},[60,907,908],{"class":94}," Debounced",[60,910,98],{"class":70},[60,912,913],{"class":101},"onSearch",[60,915,110],{"class":70},[60,917,918,920,923,925,927,929,931],{"class":62,"line":81},[60,919,808],{"class":66},[60,921,922],{"class":381}," timerRef",[60,924,498],{"class":66},[60,926,816],{"class":94},[60,928,226],{"class":70},[60,930,821],{"class":381},[60,932,824],{"class":70},[60,934,935],{"class":62,"line":88},[60,936,85],{"emptyLinePlaceholder":84},[60,938,939,941,944,946,948,950,952,954],{"class":62,"line":113},[60,940,808],{"class":66},[60,942,943],{"class":94}," handleChange",[60,945,498],{"class":66},[60,947,551],{"class":70},[60,949,174],{"class":101},[60,951,232],{"class":70},[60,953,360],{"class":66},[60,955,531],{"class":70},[60,957,958,961,964],{"class":62,"line":125},[60,959,960],{"class":94},"    clearTimeout",[60,962,963],{"class":70},"(timerRef.current)          ",[60,965,966],{"class":235},"\u002F\u002F cancel previous — no re-render\n",[60,968,969,972,974,977,979,981],{"class":62,"line":152},[60,970,971],{"class":70},"    timerRef.current ",[60,973,138],{"class":66},[60,975,976],{"class":94}," setTimeout",[60,978,539],{"class":70},[60,980,360],{"class":66},[60,982,531],{"class":70},[60,984,985,988],{"class":62,"line":189},[60,986,987],{"class":94},"      onSearch",[60,989,990],{"class":70},"(e.target.value)\n",[60,992,993,996,999],{"class":62,"line":195},[60,994,995],{"class":70},"    }, ",[60,997,998],{"class":381},"300",[60,1000,824],{"class":70},[60,1002,1003],{"class":62,"line":206},[60,1004,1005],{"class":70},"  }\n",[60,1007,1008],{"class":62,"line":217},[60,1009,85],{"emptyLinePlaceholder":84},[60,1011,1012,1014,1016,1018,1020,1022,1025],{"class":62,"line":239},[60,1013,536],{"class":94},[60,1015,539],{"class":70},[60,1017,360],{"class":66},[60,1019,618],{"class":70},[60,1021,360],{"class":66},[60,1023,1024],{"class":94}," clearTimeout",[60,1026,1027],{"class":70},"(timerRef.current), [])\n",[60,1029,1030],{"class":62,"line":245},[60,1031,85],{"emptyLinePlaceholder":84},[60,1033,1034,1036,1039,1041,1044,1046],{"class":62,"line":718},[60,1035,116],{"class":66},[60,1037,1038],{"class":70}," \u003C",[60,1040,842],{"class":131},[60,1042,1043],{"class":94}," onChange",[60,1045,138],{"class":66},[60,1047,1048],{"class":70},"{handleChange} \u002F>\n",[60,1050,1051],{"class":62,"line":724},[60,1052,248],{"class":70},[31,1054,1056],{"id":1055},"refs-vs-state-the-core-mental-model","Refs vs state — the core mental model",[1058,1059,1060,1075],"table",{},[1061,1062,1063],"thead",{},[1064,1065,1066,1069,1072],"tr",{},[1067,1068],"th",{},[1067,1070,1071],{},"State",[1067,1073,1074],{},"Ref",[1076,1077,1078,1089,1099,1110],"tbody",{},[1064,1079,1080,1084,1087],{},[1081,1082,1083],"td",{},"Persists across renders",[1081,1085,1086],{},"Yes",[1081,1088,1086],{},[1064,1090,1091,1094,1096],{},[1081,1092,1093],{},"Triggers re-render on change",[1081,1095,1086],{},[1081,1097,1098],{},"No",[1064,1100,1101,1104,1107],{},[1081,1102,1103],{},"Update timing",[1081,1105,1106],{},"Batched, asynchronous",[1081,1108,1109],{},"Synchronous",[1064,1111,1112,1115,1118],{},[1081,1113,1114],{},"Use for",[1081,1116,1117],{},"UI values that appear in JSX",[1081,1119,1120],{},"Bookkeeping invisible to the UI",[15,1122,1123,1124,1127],{},"The decision is simple: if a value needs to be ",[46,1125,1126],{},"displayed in the render output"," or\ncontrol conditional rendering, it must be state. If it's behind-the-scenes bookkeeping\n(timers, measurements, previous values), a ref keeps the render cycle clean.",[31,1129,1131],{"id":1130},"callback-refs-knowing-when-a-node-appears","Callback refs — knowing when a node appears",[15,1133,1134,1135,1137,1138,1140],{},"A callback ref is a function you pass to the ",[22,1136,524],{}," prop. React calls it with the DOM node\non mount and ",[22,1139,821],{}," on unmount — it notifies you when the attachment changes, unlike a\nref object which is silently populated after render.",[51,1142,1144],{"className":53,"code":1143,"language":55,"meta":56,"style":56},"function MeasuredBox() {\n  const [height, setHeight] = useState(0)\n\n  const measuredRef = useCallback((node) => {\n    if (node !== null) {\n      setHeight(node.getBoundingClientRect().height)\n    }\n  }, [])\n\n  return \u003Cdiv ref={measuredRef}>Height: {height}px\u003C\u002Fdiv>\n}\n",[22,1145,1146,1155,1185,1189,1213,1229,1243,1248,1253,1257,1276],{"__ignoreMap":56},[60,1147,1148,1150,1153],{"class":62,"line":63},[60,1149,91],{"class":66},[60,1151,1152],{"class":94}," MeasuredBox",[60,1154,327],{"class":70},[60,1156,1157,1159,1162,1165,1167,1170,1173,1175,1178,1180,1183],{"class":62,"line":81},[60,1158,808],{"class":66},[60,1160,1161],{"class":70}," [",[60,1163,1164],{"class":381},"height",[60,1166,104],{"class":70},[60,1168,1169],{"class":381},"setHeight",[60,1171,1172],{"class":70},"] ",[60,1174,138],{"class":66},[60,1176,1177],{"class":94}," useState",[60,1179,226],{"class":70},[60,1181,1182],{"class":381},"0",[60,1184,824],{"class":70},[60,1186,1187],{"class":62,"line":88},[60,1188,85],{"emptyLinePlaceholder":84},[60,1190,1191,1193,1196,1198,1201,1204,1207,1209,1211],{"class":62,"line":113},[60,1192,808],{"class":66},[60,1194,1195],{"class":381}," measuredRef",[60,1197,498],{"class":66},[60,1199,1200],{"class":94}," useCallback",[60,1202,1203],{"class":70},"((",[60,1205,1206],{"class":101},"node",[60,1208,232],{"class":70},[60,1210,360],{"class":66},[60,1212,531],{"class":70},[60,1214,1215,1217,1220,1223,1226],{"class":62,"line":125},[60,1216,548],{"class":66},[60,1218,1219],{"class":70}," (node ",[60,1221,1222],{"class":66},"!==",[60,1224,1225],{"class":381}," null",[60,1227,1228],{"class":70},") {\n",[60,1230,1231,1234,1237,1240],{"class":62,"line":152},[60,1232,1233],{"class":94},"      setHeight",[60,1235,1236],{"class":70},"(node.",[60,1238,1239],{"class":94},"getBoundingClientRect",[60,1241,1242],{"class":70},"().height)\n",[60,1244,1245],{"class":62,"line":189},[60,1246,1247],{"class":70},"    }\n",[60,1249,1250],{"class":62,"line":195},[60,1251,1252],{"class":70},"  }, [])\n",[60,1254,1255],{"class":62,"line":206},[60,1256,85],{"emptyLinePlaceholder":84},[60,1258,1259,1261,1263,1265,1267,1269,1272,1274],{"class":62,"line":217},[60,1260,116],{"class":66},[60,1262,1038],{"class":70},[60,1264,132],{"class":131},[60,1266,845],{"class":94},[60,1268,138],{"class":66},[60,1270,1271],{"class":70},"{measuredRef}>Height: {height}px\u003C\u002F",[60,1273,132],{"class":131},[60,1275,203],{"class":70},[60,1277,1278],{"class":62,"line":239},[60,1279,248],{"class":70},[15,1281,1282,1283,1286],{},"Use a callback ref when you need to ",[46,1284,1285],{},"react to the node being attached or detached"," —\nmeasuring conditional elements, initialising third-party libraries, or observing nodes\nthat come and go.",[31,1288,1290],{"id":1289},"ref-forwarding-and-useimperativehandle","Ref forwarding and useImperativeHandle",[15,1292,1293,1294,1296,1297,1300],{},"By default, ",[22,1295,524],{}," on a custom component is not forwarded. Use ",[22,1298,1299],{},"React.forwardRef"," to pass\nthe ref through to a DOM element inside the component.",[51,1302,1304],{"className":53,"code":1303,"language":55,"meta":56,"style":56},"const FancyInput = React.forwardRef((props, ref) => (\n  \u003Cinput {...props} ref={ref} className=\"fancy\" \u002F>\n))\n",[22,1305,1306,1334,1368],{"__ignoreMap":56},[60,1307,1308,1310,1313,1315,1317,1319,1321,1324,1326,1328,1330,1332],{"class":62,"line":63},[60,1309,493],{"class":66},[60,1311,1312],{"class":381}," FancyInput",[60,1314,498],{"class":66},[60,1316,501],{"class":70},[60,1318,504],{"class":94},[60,1320,1203],{"class":70},[60,1322,1323],{"class":101},"props",[60,1325,104],{"class":70},[60,1327,524],{"class":101},[60,1329,232],{"class":70},[60,1331,360],{"class":66},[60,1333,344],{"class":70},[60,1335,1336,1339,1341,1344,1347,1350,1352,1354,1357,1360,1362,1365],{"class":62,"line":81},[60,1337,1338],{"class":70},"  \u003C",[60,1340,842],{"class":131},[60,1342,1343],{"class":70}," {",[60,1345,1346],{"class":66},"...",[60,1348,1349],{"class":70},"props} ",[60,1351,524],{"class":94},[60,1353,138],{"class":66},[60,1355,1356],{"class":70},"{ref} ",[60,1358,1359],{"class":94},"className",[60,1361,138],{"class":66},[60,1363,1364],{"class":77},"\"fancy\"",[60,1366,1367],{"class":70}," \u002F>\n",[60,1369,1370],{"class":62,"line":88},[60,1371,1372],{"class":70},"))\n",[15,1374,1375,1376,1379,1380,1382,1383,1386],{},"When you want to expose a ",[46,1377,1378],{},"custom API"," rather than raw DOM access, combine ",[22,1381,504],{},"\nwith ",[22,1384,1385],{},"useImperativeHandle",":",[51,1388,1390],{"className":53,"code":1389,"language":55,"meta":56,"style":56},"const VideoPlayer = React.forwardRef((props, ref) => {\n  const videoRef = useRef(null)\n\n  useImperativeHandle(ref, () => ({\n    play:  () => videoRef.current.play(),\n    pause: () => videoRef.current.pause(),\n  }), [])\n\n  return \u003Cvideo ref={videoRef} src={props.src} \u002F>\n})\n",[22,1391,1392,1419,1436,1440,1453,1472,1489,1494,1498,1522],{"__ignoreMap":56},[60,1393,1394,1396,1399,1401,1403,1405,1407,1409,1411,1413,1415,1417],{"class":62,"line":63},[60,1395,493],{"class":66},[60,1397,1398],{"class":381}," VideoPlayer",[60,1400,498],{"class":66},[60,1402,501],{"class":70},[60,1404,504],{"class":94},[60,1406,1203],{"class":70},[60,1408,1323],{"class":101},[60,1410,104],{"class":70},[60,1412,524],{"class":101},[60,1414,232],{"class":70},[60,1416,360],{"class":66},[60,1418,531],{"class":70},[60,1420,1421,1423,1426,1428,1430,1432,1434],{"class":62,"line":81},[60,1422,808],{"class":66},[60,1424,1425],{"class":381}," videoRef",[60,1427,498],{"class":66},[60,1429,816],{"class":94},[60,1431,226],{"class":70},[60,1433,821],{"class":381},[60,1435,824],{"class":70},[60,1437,1438],{"class":62,"line":88},[60,1439,85],{"emptyLinePlaceholder":84},[60,1441,1442,1445,1448,1450],{"class":62,"line":113},[60,1443,1444],{"class":94},"  useImperativeHandle",[60,1446,1447],{"class":70},"(ref, () ",[60,1449,360],{"class":66},[60,1451,1452],{"class":70}," ({\n",[60,1454,1455,1458,1461,1463,1466,1469],{"class":62,"line":125},[60,1456,1457],{"class":94},"    play",[60,1459,1460],{"class":70},":  () ",[60,1462,360],{"class":66},[60,1464,1465],{"class":70}," videoRef.current.",[60,1467,1468],{"class":94},"play",[60,1470,1471],{"class":70},"(),\n",[60,1473,1474,1477,1480,1482,1484,1487],{"class":62,"line":152},[60,1475,1476],{"class":94},"    pause",[60,1478,1479],{"class":70},": () ",[60,1481,360],{"class":66},[60,1483,1465],{"class":70},[60,1485,1486],{"class":94},"pause",[60,1488,1471],{"class":70},[60,1490,1491],{"class":62,"line":189},[60,1492,1493],{"class":70},"  }), [])\n",[60,1495,1496],{"class":62,"line":195},[60,1497,85],{"emptyLinePlaceholder":84},[60,1499,1500,1502,1504,1507,1509,1511,1514,1517,1519],{"class":62,"line":206},[60,1501,116],{"class":66},[60,1503,1038],{"class":70},[60,1505,1506],{"class":131},"video",[60,1508,845],{"class":94},[60,1510,138],{"class":66},[60,1512,1513],{"class":70},"{videoRef} ",[60,1515,1516],{"class":94},"src",[60,1518,138],{"class":66},[60,1520,1521],{"class":70},"{props.src} \u002F>\n",[60,1523,1524],{"class":62,"line":217},[60,1525,747],{"class":70},[15,1527,1528,1529,447,1531,1533,1534,1537],{},"This exposes only ",[22,1530,1468],{},[22,1532,1486],{}," — not the full ",[22,1535,1536],{},"HTMLVideoElement"," — enforcing an\nintentional API boundary.",[31,1539,1541],{"id":1540},"when-refs-are-a-design-smell","When refs are a design smell",[15,1543,1544],{},"Reaching for refs often signals that a component is fighting React rather than working\nwith it:",[439,1546,1547,1553,1563,1569],{},[442,1548,1549,1552],{},[46,1550,1551],{},"Reading input values via ref instead of state"," — use a controlled component",[442,1554,1555,1558,1559,1562],{},[46,1556,1557],{},"Triggering a re-render by writing to a ref"," — use ",[22,1560,1561],{},"setState"," instead",[442,1564,1565,1568],{},[46,1566,1567],{},"Sharing data between siblings via ref"," — lift state or use context",[442,1570,1571,1574],{},[46,1572,1573],{},"Replicating what effect dependencies should handle"," — fix the dependency array",[15,1576,1577,1578,1581],{},"If you find yourself writing ",[22,1579,1580],{},"ref.current = someValue"," to communicate between components\nor drive rendering logic, there's almost certainly a declarative solution.",[15,1583,1584,1585,1588,1589,1591],{},"One important subtlety: ",[46,1586,1587],{},"never mutate refs during render",". React's concurrent mode may\nrender a component multiple times without committing — each discarded render would\nmutate the ref, leaving it in an inconsistent state. Always write to ",[22,1590,775],{}," inside\neffects or event handlers, after commit.",[10,1593,1595],{"id":1594},"key-takeaways","Key Takeaways",[439,1597,1598,1603,1606,1615,1623,1629,1632,1638,1647],{},[442,1599,1600,1602],{},[22,1601,24],{}," renders children at a different DOM location while keeping them in the\nReact component tree — context, events, and state all still flow from React ancestors.",[442,1604,1605],{},"Events bubble through the React tree, not the DOM tree — a portal's click events reach\nits React parent even though the DOM is elsewhere. This is the top interview gotcha.",[442,1607,1608,1609,1611,1612,1614],{},"Use portals for modals, tooltips, and dropdowns that need to escape ",[22,1610,273],{}," stacking\ncontexts or ",[22,1613,301],{}," clipping from ancestor elements.",[442,1616,1617,1618,104,1620,1622],{},"Always add ",[22,1619,446],{},[22,1621,450],{},", and focus management manually — portals\ndo not provide accessibility automatically.",[442,1624,1625,1628],{},[22,1626,1627],{},"useRef"," has two jobs: DOM element access and mutable value storage across renders\nwithout triggering re-renders.",[442,1630,1631],{},"Prefer state when a value drives the UI; use a ref for behind-the-scenes bookkeeping\nlike timer IDs, previous values, and resource handles.",[442,1633,1634,1635,1637],{},"Use a callback ref instead of ",[22,1636,1627],{}," when you need to run code the moment a DOM node\nappears or disappears.",[442,1639,1640,1641,1643,1644,1646],{},"Use ",[22,1642,504],{}," to expose a DOM node from a function component; add\n",[22,1645,1385],{}," to expose a controlled imperative API instead of raw DOM access.",[442,1648,1649],{},"Frequent ref usage is a design smell — it often means state, context, or better effect\ndependency management would solve the problem declaratively.",[1651,1652,1653],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":56,"searchDepth":81,"depth":81,"links":1655},[1656,1657,1663,1670],{"id":12,"depth":81,"text":13},{"id":28,"depth":81,"text":29,"children":1658},[1659,1660,1661,1662],{"id":33,"depth":88,"text":34},{"id":251,"depth":88,"text":252},{"id":305,"depth":88,"text":306},{"id":433,"depth":88,"text":434},{"id":757,"depth":81,"text":758,"children":1664},[1665,1666,1667,1668,1669],{"id":761,"depth":88,"text":762},{"id":1055,"depth":88,"text":1056},{"id":1130,"depth":88,"text":1131},{"id":1289,"depth":88,"text":1290},{"id":1540,"depth":88,"text":1541},{"id":1594,"depth":81,"text":1595},"Master React portals and refs for interviews — createPortal, DOM escape hatch, useRef, callback refs, and direct DOM manipulation patterns.","medium","md","React","react",{},"\u002Fblog\u002Freact-portals-refs-guide","\u002Freact\u002Fpatterns\u002Fportals-refs",{"title":5,"description":1671},"blog\u002Freact-portals-refs-guide","Portals & Refs","Patterns","patterns","2026-06-24","PQedRLhXPGulKjwfKlJ8kSS8x6mkWAnffv4qrCeCL_I",1782244083295]