[{"data":1,"prerenderedAt":1786},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-forward-ref-imperative-guide":3},{"id":4,"title":5,"body":6,"description":1771,"difficulty":1772,"extension":1773,"framework":1774,"frameworkSlug":1775,"meta":1776,"navigation":110,"order":160,"path":1777,"qaPath":1778,"seo":1779,"stem":1780,"subtopic":1781,"topic":1782,"topicSlug":1783,"updated":1784,"__hash__":1785},"blog\u002Fblog\u002Freact-forward-ref-imperative-guide.md","React forwardRef & useImperativeHandle — Complete Interview Guide",{"type":7,"value":8,"toc":1753},"minimark",[9,14,26,37,41,52,55,62,76,333,353,367,373,379,393,565,580,584,592,749,758,762,768,838,849,853,867,1224,1234,1241,1255,1316,1325,1333,1342,1372,1407,1411,1423,1499,1510,1514,1517,1671,1674,1678,1749],[10,11,13],"h2",{"id":12},"why-interviewers-love-this-topic","Why interviewers love this topic",[15,16,17,21,22,25],"p",{},[18,19,20],"code",{},"forwardRef"," and ",[18,23,24],{},"useImperativeHandle"," sit at an interesting boundary in React: they are\nthe framework's deliberate escape hatch from the \"data flows down\" model into the\nimperative world. Senior React interviewers ask about them because the answers reveal\nwhether a candidate truly understands React's data model, or is just comfortable with\nthe happy path.",[15,27,28,29,31,32,36],{},"The questions escalate fast. \"What is ",[18,30,20],{},"?\" is the warm-up. The real test is\n\"When ",[33,34,35],"em",{},"wouldn't"," you use an imperative handle?\" or \"Show me how you'd type this in\nTypeScript.\" This guide builds up the full mental model so you can answer from\nunderstanding, not memorisation.",[10,38,40],{"id":39},"why-function-components-cant-accept-refs-by-default","Why function components can't accept refs by default",[15,42,43,44,47,48,51],{},"Every React ref points to something. For a DOM element like ",[18,45,46],{},"\u003Cinput>",", the ref points to\nthe underlying ",[18,49,50],{},"HTMLInputElement",". For a class component, it points to the component\ninstance. Function components have neither — they are plain functions that return JSX and\nleave no persistent object behind.",[15,53,54],{},"This is intentional. React's design pushes you toward props for all parent–child\ncommunication. Refs to children are the escape hatch, and React makes you opt in\nexplicitly.",[10,56,58,61],{"id":57},"reactforwardref-the-opt-in-mechanism",[18,59,60],{},"React.forwardRef"," — the opt-in mechanism",[15,63,64,66,67,71,72,75],{},[18,65,20],{}," wraps a render function and injects the parent-supplied ref as a ",[68,69,70],"strong",{},"second\nargument",", alongside ",[18,73,74],{},"props",". The result is a component that looks and renders like any\nother but correctly threads the ref through to whatever you attach it to internally.",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-jsx shiki shiki-themes github-light github-dark","import { forwardRef } from 'react'\n\nconst FancyInput = forwardRef(function FancyInput(props, ref) {\n  \u002F\u002F attach the forwarded ref to the actual DOM input\n  return \u003Cinput ref={ref} className=\"fancy\" {...props} \u002F>\n})\n\nfunction Form() {\n  const inputRef = useRef(null)\n  return (\n    \u003C>\n      \u003CFancyInput ref={inputRef} placeholder=\"Name\" \u002F>\n      \u003Cbutton onClick={() => inputRef.current.focus()}>Focus\u003C\u002Fbutton>\n    \u003C\u002F>\n  )\n}\n","jsx","",[18,84,85,105,112,151,158,197,203,208,219,241,249,255,282,315,321,327],{"__ignoreMap":82},[86,87,90,94,98,101],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"szBVR","import",[86,95,97],{"class":96},"sVt8B"," { forwardRef } ",[86,99,100],{"class":92},"from",[86,102,104],{"class":103},"sZZnC"," 'react'\n",[86,106,108],{"class":88,"line":107},2,[86,109,111],{"emptyLinePlaceholder":110},true,"\n",[86,113,115,118,122,125,129,132,135,137,139,142,145,148],{"class":88,"line":114},3,[86,116,117],{"class":92},"const",[86,119,121],{"class":120},"sj4cs"," FancyInput",[86,123,124],{"class":92}," =",[86,126,128],{"class":127},"sScJk"," forwardRef",[86,130,131],{"class":96},"(",[86,133,134],{"class":92},"function",[86,136,121],{"class":127},[86,138,131],{"class":96},[86,140,74],{"class":141},"s4XuR",[86,143,144],{"class":96},", ",[86,146,147],{"class":141},"ref",[86,149,150],{"class":96},") {\n",[86,152,154],{"class":88,"line":153},4,[86,155,157],{"class":156},"sJ8bj","  \u002F\u002F attach the forwarded ref to the actual DOM input\n",[86,159,161,164,167,171,174,177,180,183,185,188,191,194],{"class":88,"line":160},5,[86,162,163],{"class":92},"  return",[86,165,166],{"class":96}," \u003C",[86,168,170],{"class":169},"s9eBZ","input",[86,172,173],{"class":127}," ref",[86,175,176],{"class":92},"=",[86,178,179],{"class":96},"{ref} ",[86,181,182],{"class":127},"className",[86,184,176],{"class":92},[86,186,187],{"class":103},"\"fancy\"",[86,189,190],{"class":96}," {",[86,192,193],{"class":92},"...",[86,195,196],{"class":96},"props} \u002F>\n",[86,198,200],{"class":88,"line":199},6,[86,201,202],{"class":96},"})\n",[86,204,206],{"class":88,"line":205},7,[86,207,111],{"emptyLinePlaceholder":110},[86,209,211,213,216],{"class":88,"line":210},8,[86,212,134],{"class":92},[86,214,215],{"class":127}," Form",[86,217,218],{"class":96},"() {\n",[86,220,222,225,228,230,233,235,238],{"class":88,"line":221},9,[86,223,224],{"class":92},"  const",[86,226,227],{"class":120}," inputRef",[86,229,124],{"class":92},[86,231,232],{"class":127}," useRef",[86,234,131],{"class":96},[86,236,237],{"class":120},"null",[86,239,240],{"class":96},")\n",[86,242,244,246],{"class":88,"line":243},10,[86,245,163],{"class":92},[86,247,248],{"class":96}," (\n",[86,250,252],{"class":88,"line":251},11,[86,253,254],{"class":96},"    \u003C>\n",[86,256,258,261,264,266,268,271,274,276,279],{"class":88,"line":257},12,[86,259,260],{"class":96},"      \u003C",[86,262,263],{"class":120},"FancyInput",[86,265,173],{"class":127},[86,267,176],{"class":92},[86,269,270],{"class":96},"{inputRef} ",[86,272,273],{"class":127},"placeholder",[86,275,176],{"class":92},[86,277,278],{"class":103},"\"Name\"",[86,280,281],{"class":96}," \u002F>\n",[86,283,285,287,290,293,295,298,301,304,307,310,312],{"class":88,"line":284},13,[86,286,260],{"class":96},[86,288,289],{"class":169},"button",[86,291,292],{"class":127}," onClick",[86,294,176],{"class":92},[86,296,297],{"class":96},"{() ",[86,299,300],{"class":92},"=>",[86,302,303],{"class":96}," inputRef.current.",[86,305,306],{"class":127},"focus",[86,308,309],{"class":96},"()}>Focus\u003C\u002F",[86,311,289],{"class":169},[86,313,314],{"class":96},">\n",[86,316,318],{"class":88,"line":317},14,[86,319,320],{"class":96},"    \u003C\u002F>\n",[86,322,324],{"class":88,"line":323},15,[86,325,326],{"class":96},"  )\n",[86,328,330],{"class":88,"line":329},16,[86,331,332],{"class":96},"}\n",[15,334,335,336,338,339,341,342,345,346,349,350,352],{},"Without ",[18,337,20],{},", the ",[18,340,147],{}," attribute on ",[18,343,344],{},"\u003CFancyInput>"," would silently be dropped and\n",[18,347,348],{},"inputRef.current"," would remain ",[18,351,237],{},".",[15,354,355,356,359,360,362,363,366],{},"Always use a ",[68,357,358],{},"named function expression"," inside ",[18,361,20],{},". React DevTools uses the\nfunction name as the display name — anonymous wrappers show up as ",[18,364,365],{},"ForwardRef",", which\nmakes debugging miserable.",[10,368,370,372],{"id":369},"useimperativehandle-curating-the-public-api",[18,371,24],{}," — curating the public API",[15,374,375,376,378],{},"Forwarding the raw DOM ref works fine for thin wrappers, but it gives the parent\nunrestricted access to the node. Any method that exists on ",[18,377,50],{}," is now\naccessible — including ones you never intended to expose.",[15,380,381,384,385,388,389,392],{},[18,382,383],{},"useImperativeHandle(ref, createHandle, deps?)"," solves this by ",[68,386,387],{},"replacing"," what the\nparent sees at ",[18,390,391],{},"ref.current"," with a custom object you define:",[77,394,396],{"className":79,"code":395,"language":81,"meta":82,"style":82},"const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {\n  const videoRef = useRef(null)   \u002F\u002F private — never reaches the parent\n\n  useImperativeHandle(ref, () => ({\n    play:  () => videoRef.current.play(),\n    pause: () => videoRef.current.pause(),\n    seek:  (t) => { videoRef.current.currentTime = t },\n  }), [])  \u002F\u002F empty deps — handle is stable\n\n  return \u003Cvideo ref={videoRef} src={src} \u002F>\n})\n",[18,397,398,428,449,453,466,485,502,526,534,538,561],{"__ignoreMap":82},[86,399,400,402,405,407,409,411,413,415,418,421,424,426],{"class":88,"line":89},[86,401,117],{"class":92},[86,403,404],{"class":120}," VideoPlayer",[86,406,124],{"class":92},[86,408,128],{"class":127},[86,410,131],{"class":96},[86,412,134],{"class":92},[86,414,404],{"class":127},[86,416,417],{"class":96},"({ ",[86,419,420],{"class":141},"src",[86,422,423],{"class":96}," }, ",[86,425,147],{"class":141},[86,427,150],{"class":96},[86,429,430,432,435,437,439,441,443,446],{"class":88,"line":107},[86,431,224],{"class":92},[86,433,434],{"class":120}," videoRef",[86,436,124],{"class":92},[86,438,232],{"class":127},[86,440,131],{"class":96},[86,442,237],{"class":120},[86,444,445],{"class":96},")   ",[86,447,448],{"class":156},"\u002F\u002F private — never reaches the parent\n",[86,450,451],{"class":88,"line":114},[86,452,111],{"emptyLinePlaceholder":110},[86,454,455,458,461,463],{"class":88,"line":153},[86,456,457],{"class":127},"  useImperativeHandle",[86,459,460],{"class":96},"(ref, () ",[86,462,300],{"class":92},[86,464,465],{"class":96}," ({\n",[86,467,468,471,474,476,479,482],{"class":88,"line":160},[86,469,470],{"class":127},"    play",[86,472,473],{"class":96},":  () ",[86,475,300],{"class":92},[86,477,478],{"class":96}," videoRef.current.",[86,480,481],{"class":127},"play",[86,483,484],{"class":96},"(),\n",[86,486,487,490,493,495,497,500],{"class":88,"line":199},[86,488,489],{"class":127},"    pause",[86,491,492],{"class":96},": () ",[86,494,300],{"class":92},[86,496,478],{"class":96},[86,498,499],{"class":127},"pause",[86,501,484],{"class":96},[86,503,504,507,510,513,516,518,521,523],{"class":88,"line":205},[86,505,506],{"class":127},"    seek",[86,508,509],{"class":96},":  (",[86,511,512],{"class":141},"t",[86,514,515],{"class":96},") ",[86,517,300],{"class":92},[86,519,520],{"class":96}," { videoRef.current.currentTime ",[86,522,176],{"class":92},[86,524,525],{"class":96}," t },\n",[86,527,528,531],{"class":88,"line":210},[86,529,530],{"class":96},"  }), [])  ",[86,532,533],{"class":156},"\u002F\u002F empty deps — handle is stable\n",[86,535,536],{"class":88,"line":221},[86,537,111],{"emptyLinePlaceholder":110},[86,539,540,542,544,547,549,551,554,556,558],{"class":88,"line":243},[86,541,163],{"class":92},[86,543,166],{"class":96},[86,545,546],{"class":169},"video",[86,548,173],{"class":127},[86,550,176],{"class":92},[86,552,553],{"class":96},"{videoRef} ",[86,555,420],{"class":127},[86,557,176],{"class":92},[86,559,560],{"class":96},"{src} \u002F>\n",[86,562,563],{"class":88,"line":251},[86,564,202],{"class":96},[15,566,567,568,571,572,575,576,579],{},"The parent can now call ",[18,569,570],{},"player.current.play()"," but cannot touch ",[18,573,574],{},"videoRef.current","\ndirectly. The ",[18,577,578],{},"\u003Cvideo>"," element can be swapped for any other implementation without\nbreaking the parent.",[10,581,583],{"id":582},"the-full-pattern-combined","The full pattern combined",[15,585,586,587,589,590,352],{},"The canonical recipe has four parts: keep an internal DOM ref private, wire it up to the\nDOM element, expose only selected methods via ",[18,588,24],{},", and wrap the whole\nthing in ",[18,591,20],{},[77,593,595],{"className":79,"code":594,"language":81,"meta":82,"style":82},"const SmartInput = forwardRef(function SmartInput(props, ref) {\n  const inputRef = useRef(null)            \u002F\u002F 1. private ref\n\n  useImperativeHandle(ref, () => ({        \u002F\u002F 2. curated handle\n    focus: () => inputRef.current.focus(),\n    blur:  () => inputRef.current.blur(),\n    clear: () => { inputRef.current.value = '' },\n  }), [])\n\n  return \u003Cinput ref={inputRef} {...props} \u002F>  \u002F\u002F 3. wire private ref\n})\n",[18,596,597,624,644,648,662,677,693,713,718,722,745],{"__ignoreMap":82},[86,598,599,601,604,606,608,610,612,614,616,618,620,622],{"class":88,"line":89},[86,600,117],{"class":92},[86,602,603],{"class":120}," SmartInput",[86,605,124],{"class":92},[86,607,128],{"class":127},[86,609,131],{"class":96},[86,611,134],{"class":92},[86,613,603],{"class":127},[86,615,131],{"class":96},[86,617,74],{"class":141},[86,619,144],{"class":96},[86,621,147],{"class":141},[86,623,150],{"class":96},[86,625,626,628,630,632,634,636,638,641],{"class":88,"line":107},[86,627,224],{"class":92},[86,629,227],{"class":120},[86,631,124],{"class":92},[86,633,232],{"class":127},[86,635,131],{"class":96},[86,637,237],{"class":120},[86,639,640],{"class":96},")            ",[86,642,643],{"class":156},"\u002F\u002F 1. private ref\n",[86,645,646],{"class":88,"line":114},[86,647,111],{"emptyLinePlaceholder":110},[86,649,650,652,654,656,659],{"class":88,"line":153},[86,651,457],{"class":127},[86,653,460],{"class":96},[86,655,300],{"class":92},[86,657,658],{"class":96}," ({        ",[86,660,661],{"class":156},"\u002F\u002F 2. curated handle\n",[86,663,664,667,669,671,673,675],{"class":88,"line":160},[86,665,666],{"class":127},"    focus",[86,668,492],{"class":96},[86,670,300],{"class":92},[86,672,303],{"class":96},[86,674,306],{"class":127},[86,676,484],{"class":96},[86,678,679,682,684,686,688,691],{"class":88,"line":199},[86,680,681],{"class":127},"    blur",[86,683,473],{"class":96},[86,685,300],{"class":92},[86,687,303],{"class":96},[86,689,690],{"class":127},"blur",[86,692,484],{"class":96},[86,694,695,698,700,702,705,707,710],{"class":88,"line":205},[86,696,697],{"class":127},"    clear",[86,699,492],{"class":96},[86,701,300],{"class":92},[86,703,704],{"class":96}," { inputRef.current.value ",[86,706,176],{"class":92},[86,708,709],{"class":103}," ''",[86,711,712],{"class":96}," },\n",[86,714,715],{"class":88,"line":210},[86,716,717],{"class":96},"  }), [])\n",[86,719,720],{"class":88,"line":221},[86,721,111],{"emptyLinePlaceholder":110},[86,723,724,726,728,730,732,734,737,739,742],{"class":88,"line":243},[86,725,163],{"class":92},[86,727,166],{"class":96},[86,729,170],{"class":169},[86,731,173],{"class":127},[86,733,176],{"class":92},[86,735,736],{"class":96},"{inputRef} {",[86,738,193],{"class":92},[86,740,741],{"class":96},"props} \u002F>  ",[86,743,744],{"class":156},"\u002F\u002F 3. wire private ref\n",[86,746,747],{"class":88,"line":251},[86,748,202],{"class":96},[15,750,751,752,754,755,757],{},"This is the answer interviewers are hoping to hear when they ask \"show me ",[18,753,20],{}," and\n",[18,756,24],{}," working together.\"",[10,759,761],{"id":760},"when-to-use-an-imperative-handle-and-when-not-to","When to use an imperative handle — and when not to",[15,763,764,765],{},"The golden rule: ",[68,766,767],{},"use an imperative handle for actions, not for data.",[769,770,771,784],"table",{},[772,773,774],"thead",{},[775,776,777,781],"tr",{},[778,779,780],"th",{},"Appropriate (action)",[778,782,783],{},"Avoid (data)",[785,786,787,801,813,826],"tbody",{},[775,788,789,795],{},[790,791,792],"td",{},[18,793,794],{},"input.focus()",[790,796,797,800],{},[18,798,799],{},"list.setItems(data)"," — use a prop",[775,802,803,808],{},[790,804,805],{},[18,806,807],{},"player.play()",[790,809,810,800],{},[18,811,812],{},"modal.setTitle(t)",[775,814,815,820],{},[790,816,817],{},[18,818,819],{},"dialog.open()",[790,821,822,825],{},[18,823,824],{},"form.getValues()"," — use a callback\u002Fstate",[775,827,828,833],{},[790,829,830],{},[18,831,832],{},"list.scrollTo(3)",[790,834,835,800],{},[18,836,837],{},"chart.updateColor(c)",[15,839,840,841,844,845,848],{},"If the parent needs to ",[33,842,843],{},"change what renders",", use a prop. If it needs to ",[33,846,847],{},"trigger an\nevent",", an imperative handle is appropriate.",[10,850,852],{"id":851},"typescript-typing","TypeScript typing",[15,854,855,856,858,859,862,863,866],{},"The type parameters for ",[18,857,20],{}," are ",[18,860,861],{},"\u003CHandleType, PropsType>"," — handle first, props\nsecond. Always export the handle interface so consumers can type their ",[18,864,865],{},"useRef"," calls.",[77,868,872],{"className":869,"code":870,"language":871,"meta":82,"style":82},"language-tsx shiki shiki-themes github-light github-dark","export interface ModalHandle {\n  open(): void\n  close(): void\n}\n\ninterface ModalProps {\n  title: string\n  children: React.ReactNode\n}\n\nconst Modal = forwardRef\u003CModalHandle, ModalProps>(function Modal(\n  { title, children },\n  ref\n) {\n  const [visible, setVisible] = React.useState(false)\n\n  useImperativeHandle(ref, () => ({\n    open:  () => setVisible(true),\n    close: () => setVisible(false),\n  }))\n\n  if (!visible) return null\n  return \u003Cdialog open>\u003Ch2>{title}\u003C\u002Fh2>{children}\u003C\u002Fdialog>\n})\n\n\u002F\u002F Consumer\nconst modalRef = React.useRef\u003CModalHandle>(null)\nmodalRef.current?.open()   \u002F\u002F fully typed\n","tsx",[18,873,874,888,902,913,917,921,931,941,956,960,964,996,1011,1016,1020,1053,1057,1068,1089,1107,1113,1118,1139,1169,1174,1179,1185,1209],{"__ignoreMap":82},[86,875,876,879,882,885],{"class":88,"line":89},[86,877,878],{"class":92},"export",[86,880,881],{"class":92}," interface",[86,883,884],{"class":127}," ModalHandle",[86,886,887],{"class":96}," {\n",[86,889,890,893,896,899],{"class":88,"line":107},[86,891,892],{"class":127},"  open",[86,894,895],{"class":96},"()",[86,897,898],{"class":92},":",[86,900,901],{"class":120}," void\n",[86,903,904,907,909,911],{"class":88,"line":114},[86,905,906],{"class":127},"  close",[86,908,895],{"class":96},[86,910,898],{"class":92},[86,912,901],{"class":120},[86,914,915],{"class":88,"line":153},[86,916,332],{"class":96},[86,918,919],{"class":88,"line":160},[86,920,111],{"emptyLinePlaceholder":110},[86,922,923,926,929],{"class":88,"line":199},[86,924,925],{"class":92},"interface",[86,927,928],{"class":127}," ModalProps",[86,930,887],{"class":96},[86,932,933,936,938],{"class":88,"line":205},[86,934,935],{"class":141},"  title",[86,937,898],{"class":92},[86,939,940],{"class":120}," string\n",[86,942,943,946,948,951,953],{"class":88,"line":210},[86,944,945],{"class":141},"  children",[86,947,898],{"class":92},[86,949,950],{"class":127}," React",[86,952,352],{"class":96},[86,954,955],{"class":127},"ReactNode\n",[86,957,958],{"class":88,"line":221},[86,959,332],{"class":96},[86,961,962],{"class":88,"line":243},[86,963,111],{"emptyLinePlaceholder":110},[86,965,966,968,971,973,975,978,981,983,986,989,991,993],{"class":88,"line":251},[86,967,117],{"class":92},[86,969,970],{"class":120}," Modal",[86,972,124],{"class":92},[86,974,128],{"class":127},[86,976,977],{"class":96},"\u003C",[86,979,980],{"class":127},"ModalHandle",[86,982,144],{"class":96},[86,984,985],{"class":127},"ModalProps",[86,987,988],{"class":96},">(",[86,990,134],{"class":92},[86,992,970],{"class":127},[86,994,995],{"class":96},"(\n",[86,997,998,1001,1004,1006,1009],{"class":88,"line":257},[86,999,1000],{"class":96},"  { ",[86,1002,1003],{"class":141},"title",[86,1005,144],{"class":96},[86,1007,1008],{"class":141},"children",[86,1010,712],{"class":96},[86,1012,1013],{"class":88,"line":284},[86,1014,1015],{"class":141},"  ref\n",[86,1017,1018],{"class":88,"line":317},[86,1019,150],{"class":96},[86,1021,1022,1024,1027,1030,1032,1035,1038,1040,1043,1046,1048,1051],{"class":88,"line":323},[86,1023,224],{"class":92},[86,1025,1026],{"class":96}," [",[86,1028,1029],{"class":120},"visible",[86,1031,144],{"class":96},[86,1033,1034],{"class":120},"setVisible",[86,1036,1037],{"class":96},"] ",[86,1039,176],{"class":92},[86,1041,1042],{"class":96}," React.",[86,1044,1045],{"class":127},"useState",[86,1047,131],{"class":96},[86,1049,1050],{"class":120},"false",[86,1052,240],{"class":96},[86,1054,1055],{"class":88,"line":329},[86,1056,111],{"emptyLinePlaceholder":110},[86,1058,1060,1062,1064,1066],{"class":88,"line":1059},17,[86,1061,457],{"class":127},[86,1063,460],{"class":96},[86,1065,300],{"class":92},[86,1067,465],{"class":96},[86,1069,1071,1074,1076,1078,1081,1083,1086],{"class":88,"line":1070},18,[86,1072,1073],{"class":127},"    open",[86,1075,473],{"class":96},[86,1077,300],{"class":92},[86,1079,1080],{"class":127}," setVisible",[86,1082,131],{"class":96},[86,1084,1085],{"class":120},"true",[86,1087,1088],{"class":96},"),\n",[86,1090,1092,1095,1097,1099,1101,1103,1105],{"class":88,"line":1091},19,[86,1093,1094],{"class":127},"    close",[86,1096,492],{"class":96},[86,1098,300],{"class":92},[86,1100,1080],{"class":127},[86,1102,131],{"class":96},[86,1104,1050],{"class":120},[86,1106,1088],{"class":96},[86,1108,1110],{"class":88,"line":1109},20,[86,1111,1112],{"class":96},"  }))\n",[86,1114,1116],{"class":88,"line":1115},21,[86,1117,111],{"emptyLinePlaceholder":110},[86,1119,1121,1124,1127,1130,1133,1136],{"class":88,"line":1120},22,[86,1122,1123],{"class":92},"  if",[86,1125,1126],{"class":96}," (",[86,1128,1129],{"class":92},"!",[86,1131,1132],{"class":96},"visible) ",[86,1134,1135],{"class":92},"return",[86,1137,1138],{"class":120}," null\n",[86,1140,1142,1144,1146,1149,1152,1155,1157,1160,1162,1165,1167],{"class":88,"line":1141},23,[86,1143,163],{"class":92},[86,1145,166],{"class":96},[86,1147,1148],{"class":169},"dialog",[86,1150,1151],{"class":127}," open",[86,1153,1154],{"class":96},">\u003C",[86,1156,10],{"class":169},[86,1158,1159],{"class":96},">{title}\u003C\u002F",[86,1161,10],{"class":169},[86,1163,1164],{"class":96},">{children}\u003C\u002F",[86,1166,1148],{"class":169},[86,1168,314],{"class":96},[86,1170,1172],{"class":88,"line":1171},24,[86,1173,202],{"class":96},[86,1175,1177],{"class":88,"line":1176},25,[86,1178,111],{"emptyLinePlaceholder":110},[86,1180,1182],{"class":88,"line":1181},26,[86,1183,1184],{"class":156},"\u002F\u002F Consumer\n",[86,1186,1188,1190,1193,1195,1197,1199,1201,1203,1205,1207],{"class":88,"line":1187},27,[86,1189,117],{"class":92},[86,1191,1192],{"class":120}," modalRef",[86,1194,124],{"class":92},[86,1196,1042],{"class":96},[86,1198,865],{"class":127},[86,1200,977],{"class":96},[86,1202,980],{"class":127},[86,1204,988],{"class":96},[86,1206,237],{"class":120},[86,1208,240],{"class":96},[86,1210,1212,1215,1218,1221],{"class":88,"line":1211},28,[86,1213,1214],{"class":96},"modalRef.current?.",[86,1216,1217],{"class":127},"open",[86,1219,1220],{"class":96},"()   ",[86,1222,1223],{"class":156},"\u002F\u002F fully typed\n",[15,1225,1226,1227,1230,1231,352],{},"Inside the render function, the parameter type for the forwarded ref is\n",[18,1228,1229],{},"ForwardedRef\u003CT>"," (a union of callback ref, object ref, and null), not ",[18,1232,1233],{},"Ref\u003CT>",[10,1235,1237,1238],{"id":1236},"composing-with-reactmemo","Composing with ",[18,1239,1240],{},"React.memo",[15,1242,1243,1244,21,1247,1249,1250,1252,1253,898],{},"Both ",[18,1245,1246],{},"memo",[18,1248,20],{}," are higher-order wrappers. Apply ",[18,1251,20],{}," first\n(innermost), then ",[18,1254,1246],{},[77,1256,1258],{"className":79,"code":1257,"language":81,"meta":82,"style":82},"const MemoInput = memo(forwardRef(function MemoInput(props, ref) {\n  return \u003Cinput ref={ref} {...props} \u002F>\n}))\n",[18,1259,1260,1292,1311],{"__ignoreMap":82},[86,1261,1262,1264,1267,1269,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290],{"class":88,"line":89},[86,1263,117],{"class":92},[86,1265,1266],{"class":120}," MemoInput",[86,1268,124],{"class":92},[86,1270,1271],{"class":127}," memo",[86,1273,131],{"class":96},[86,1275,20],{"class":127},[86,1277,131],{"class":96},[86,1279,134],{"class":92},[86,1281,1266],{"class":127},[86,1283,131],{"class":96},[86,1285,74],{"class":141},[86,1287,144],{"class":96},[86,1289,147],{"class":141},[86,1291,150],{"class":96},[86,1293,1294,1296,1298,1300,1302,1304,1307,1309],{"class":88,"line":107},[86,1295,163],{"class":92},[86,1297,166],{"class":96},[86,1299,170],{"class":169},[86,1301,173],{"class":127},[86,1303,176],{"class":92},[86,1305,1306],{"class":96},"{ref} {",[86,1308,193],{"class":92},[86,1310,196],{"class":96},[86,1312,1313],{"class":88,"line":114},[86,1314,1315],{"class":96},"}))\n",[15,1317,1318,1319,1321,1322,1324],{},"If you accidentally apply ",[18,1320,1246],{}," first and then ",[18,1323,20],{},", it still works in practice\nbecause React resolves the wrapper chain, but the conventional order makes the intent\nclear.",[10,1326,1328,1329,1332],{"id":1327},"the-deps-array-matters","The ",[18,1330,1331],{},"deps"," array matters",[15,1334,1335,1337,1338,1341],{},[18,1336,24],{},"'s third argument works exactly like ",[18,1339,1340],{},"useEffect","'s dependency array.",[1343,1344,1345,1354,1366],"ul",{},[1346,1347,1348,1353],"li",{},[68,1349,1350],{},[18,1351,1352],{},"[]"," — create the handle once. Safe when the methods only call setters or refs\n(which are stable across renders).",[1346,1355,1356,1361,1362,1365],{},[68,1357,1358],{},[18,1359,1360],{},"[count]"," — recreate the handle when ",[18,1363,1364],{},"count"," changes. Necessary when a method\ncloses over a state value and you need it to be current.",[1346,1367,1368,1371],{},[68,1369,1370],{},"omitted"," — recreate on every render. Wasteful but correct.",[77,1373,1375],{"className":79,"code":1374,"language":81,"meta":82,"style":82},"useImperativeHandle(ref, () => ({\n  getCount: () => count,   \u002F\u002F closes over count — needs count in deps\n}), [count])\n",[18,1376,1377,1387,1402],{"__ignoreMap":82},[86,1378,1379,1381,1383,1385],{"class":88,"line":89},[86,1380,24],{"class":127},[86,1382,460],{"class":96},[86,1384,300],{"class":92},[86,1386,465],{"class":96},[86,1388,1389,1392,1394,1396,1399],{"class":88,"line":107},[86,1390,1391],{"class":127},"  getCount",[86,1393,492],{"class":96},[86,1395,300],{"class":92},[86,1397,1398],{"class":96}," count,   ",[86,1400,1401],{"class":156},"\u002F\u002F closes over count — needs count in deps\n",[86,1403,1404],{"class":88,"line":114},[86,1405,1406],{"class":96},"}), [count])\n",[10,1408,1410],{"id":1409},"react-19-changes","React 19 changes",[15,1412,1413,1414,1416,1417,1420,1421,898],{},"React 19 promotes ",[18,1415,147],{}," to a ",[68,1418,1419],{},"first-class prop",". Function components can receive it\ndirectly without ",[18,1422,20],{},[77,1424,1426],{"className":869,"code":1425,"language":871,"meta":82,"style":82},"\u002F\u002F React 19 — no forwardRef needed\nfunction FancyInput({ ref, ...props }: React.ComponentProps\u003C'input'>) {\n  return \u003Cinput ref={ref} className=\"fancy\" {...props} \u002F>\n}\n",[18,1427,1428,1433,1469,1495],{"__ignoreMap":82},[86,1429,1430],{"class":88,"line":89},[86,1431,1432],{"class":156},"\u002F\u002F React 19 — no forwardRef needed\n",[86,1434,1435,1437,1439,1441,1443,1445,1447,1449,1452,1454,1456,1458,1461,1463,1466],{"class":88,"line":107},[86,1436,134],{"class":92},[86,1438,121],{"class":127},[86,1440,417],{"class":96},[86,1442,147],{"class":141},[86,1444,144],{"class":96},[86,1446,193],{"class":92},[86,1448,74],{"class":141},[86,1450,1451],{"class":96}," }",[86,1453,898],{"class":92},[86,1455,950],{"class":127},[86,1457,352],{"class":96},[86,1459,1460],{"class":127},"ComponentProps",[86,1462,977],{"class":96},[86,1464,1465],{"class":103},"'input'",[86,1467,1468],{"class":96},">) {\n",[86,1470,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491,1493],{"class":88,"line":114},[86,1472,163],{"class":92},[86,1474,166],{"class":96},[86,1476,170],{"class":169},[86,1478,173],{"class":127},[86,1480,176],{"class":92},[86,1482,179],{"class":96},[86,1484,182],{"class":127},[86,1486,176],{"class":92},[86,1488,187],{"class":103},[86,1490,190],{"class":96},[86,1492,193],{"class":92},[86,1494,196],{"class":96},[86,1496,1497],{"class":88,"line":153},[86,1498,332],{"class":96},[15,1500,1501,1503,1504,1506,1507,1509],{},[18,1502,20],{}," still works in React 19 (it's not removed, just legacy). ",[18,1505,24],{},"\nis unchanged and continues to work with the ref prop directly. For codebases targeting\nReact 18 and earlier, ",[18,1508,20],{}," remains mandatory.",[10,1511,1513],{"id":1512},"testing-imperative-handles","Testing imperative handles",[15,1515,1516],{},"React Testing Library works well here. Create a ref, render the component with it, then\ncall the handle method and assert the observable result:",[77,1518,1520],{"className":79,"code":1519,"language":81,"meta":82,"style":82},"import { createRef } from 'react'\nimport { render, screen } from '@testing-library\u002Freact'\nimport SmartInput from '.\u002FSmartInput'\n\ntest('clear() empties the input', () => {\n  const ref = createRef()\n  render(\u003CSmartInput ref={ref} defaultValue=\"hello\" data-testid=\"inp\" \u002F>)\n  ref.current.clear()\n  expect(screen.getByTestId('inp').value).toBe('')\n})\n",[18,1521,1522,1533,1545,1557,1561,1578,1592,1628,1638,1667],{"__ignoreMap":82},[86,1523,1524,1526,1529,1531],{"class":88,"line":89},[86,1525,93],{"class":92},[86,1527,1528],{"class":96}," { createRef } ",[86,1530,100],{"class":92},[86,1532,104],{"class":103},[86,1534,1535,1537,1540,1542],{"class":88,"line":107},[86,1536,93],{"class":92},[86,1538,1539],{"class":96}," { render, screen } ",[86,1541,100],{"class":92},[86,1543,1544],{"class":103}," '@testing-library\u002Freact'\n",[86,1546,1547,1549,1552,1554],{"class":88,"line":114},[86,1548,93],{"class":92},[86,1550,1551],{"class":96}," SmartInput ",[86,1553,100],{"class":92},[86,1555,1556],{"class":103}," '.\u002FSmartInput'\n",[86,1558,1559],{"class":88,"line":153},[86,1560,111],{"emptyLinePlaceholder":110},[86,1562,1563,1566,1568,1571,1574,1576],{"class":88,"line":160},[86,1564,1565],{"class":127},"test",[86,1567,131],{"class":96},[86,1569,1570],{"class":103},"'clear() empties the input'",[86,1572,1573],{"class":96},", () ",[86,1575,300],{"class":92},[86,1577,887],{"class":96},[86,1579,1580,1582,1584,1586,1589],{"class":88,"line":199},[86,1581,224],{"class":92},[86,1583,173],{"class":120},[86,1585,124],{"class":92},[86,1587,1588],{"class":127}," createRef",[86,1590,1591],{"class":96},"()\n",[86,1593,1594,1597,1600,1603,1605,1607,1609,1612,1614,1617,1620,1622,1625],{"class":88,"line":205},[86,1595,1596],{"class":127},"  render",[86,1598,1599],{"class":96},"(\u003C",[86,1601,1602],{"class":120},"SmartInput",[86,1604,173],{"class":127},[86,1606,176],{"class":92},[86,1608,179],{"class":96},[86,1610,1611],{"class":127},"defaultValue",[86,1613,176],{"class":92},[86,1615,1616],{"class":103},"\"hello\"",[86,1618,1619],{"class":127}," data-testid",[86,1621,176],{"class":92},[86,1623,1624],{"class":103},"\"inp\"",[86,1626,1627],{"class":96}," \u002F>)\n",[86,1629,1630,1633,1636],{"class":88,"line":210},[86,1631,1632],{"class":96},"  ref.current.",[86,1634,1635],{"class":127},"clear",[86,1637,1591],{"class":96},[86,1639,1640,1643,1646,1649,1651,1654,1657,1660,1662,1665],{"class":88,"line":221},[86,1641,1642],{"class":127},"  expect",[86,1644,1645],{"class":96},"(screen.",[86,1647,1648],{"class":127},"getByTestId",[86,1650,131],{"class":96},[86,1652,1653],{"class":103},"'inp'",[86,1655,1656],{"class":96},").value).",[86,1658,1659],{"class":127},"toBe",[86,1661,131],{"class":96},[86,1663,1664],{"class":103},"''",[86,1666,240],{"class":96},[86,1668,1669],{"class":88,"line":243},[86,1670,202],{"class":96},[15,1672,1673],{},"Test what the handle does (observable DOM behaviour), not how it is implemented.",[10,1675,1677],{"id":1676},"key-takeaways","Key Takeaways",[1343,1679,1680,1692,1702,1711,1721,1728,1737,1746],{},[1346,1681,1682,1683,1685,1686,1688,1689,1691],{},"Function components have no instance, so they can't accept a ",[18,1684,147],{}," without explicitly\nopting in via ",[18,1687,20],{}," (React ≤ 18) or a ",[18,1690,147],{}," prop (React 19+).",[1346,1693,1694,1697,1698,1701],{},[18,1695,1696],{},"forwardRef(fn)"," passes the parent's ref as the second argument to ",[18,1699,1700],{},"fn",", letting you\nattach it to any DOM node or child component inside.",[1346,1703,1704,1707,1708,1710],{},[18,1705,1706],{},"useImperativeHandle(ref, createHandle, deps)"," replaces ",[18,1709,391],{}," with a custom\nobject — use it to expose a curated, stable API instead of the raw DOM node.",[1346,1712,1713,1714,1717,1718,1720],{},"Prefer imperative handles for ",[68,1715,1716],{},"actions"," (focus, play, scroll, open); prefer ",[68,1719,74],{},"\nfor data changes.",[1346,1722,1723,1724,1727],{},"TypeScript: ",[18,1725,1726],{},"forwardRef\u003CHandleType, PropsType>"," — handle type first, props second.\nExport the handle interface so consumers can type their refs.",[1346,1729,1730,1731,1733,1734,352],{},"Compose with ",[18,1732,1246],{}," as ",[18,1735,1736],{},"memo(forwardRef(...))",[1346,1738,1739,1740,1742,1743,1745],{},"React 19 makes ",[18,1741,147],{}," a regular prop, deprecating ",[18,1744,20],{}," for new code.",[1346,1747,1748],{},"Test handles by asserting observable DOM or state changes, not internal implementation.",[1750,1751,1752],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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);}",{"title":82,"searchDepth":107,"depth":107,"links":1754},[1755,1756,1757,1759,1761,1762,1763,1764,1766,1768,1769,1770],{"id":12,"depth":107,"text":13},{"id":39,"depth":107,"text":40},{"id":57,"depth":107,"text":1758},"React.forwardRef — the opt-in mechanism",{"id":369,"depth":107,"text":1760},"useImperativeHandle — curating the public API",{"id":582,"depth":107,"text":583},{"id":760,"depth":107,"text":761},{"id":851,"depth":107,"text":852},{"id":1236,"depth":107,"text":1765},"Composing with React.memo",{"id":1327,"depth":107,"text":1767},"The deps array matters",{"id":1409,"depth":107,"text":1410},{"id":1512,"depth":107,"text":1513},{"id":1676,"depth":107,"text":1677},"Master React forwardRef and useImperativeHandle for interviews — ref forwarding, imperative API design, focus control, TypeScript typing, and React 19 changes.","hard","md","React","react",{},"\u002Fblog\u002Freact-forward-ref-imperative-guide","\u002Freact\u002Fpatterns\u002Fforward-ref-imperative",{"title":5,"description":1771},"blog\u002Freact-forward-ref-imperative-guide","forwardRef & useImperativeHandle","Patterns","patterns","2026-06-24","EcG1dBOa5Idd6je8ONvSzNp0n7TC7xXQxIe3F0YTg9k",1782244083323]