[{"data":1,"prerenderedAt":1791},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-render-props-hoc-guide":3},{"id":4,"title":5,"body":6,"description":1776,"difficulty":1777,"extension":1778,"framework":1779,"frameworkSlug":1780,"meta":1781,"navigation":147,"order":80,"path":1782,"qaPath":1783,"seo":1784,"stem":1785,"subtopic":1786,"topic":1787,"topicSlug":1788,"updated":1789,"__hash__":1790},"blog\u002Fblog\u002Freact-render-props-hoc-guide.md","React Render Props & HOCs — Complete Interview Guide",{"type":7,"value":8,"toc":1759},"minimark",[9,14,27,31,38,348,353,370,431,447,451,461,737,747,751,758,780,783,787,794,909,913,920,1013,1034,1038,1045,1049,1052,1127,1314,1318,1321,1349,1353,1356,1369,1386,1454,1458,1468,1475,1587,1591,1598,1685,1689,1755],[10,11,13],"h2",{"id":12},"the-logic-reuse-problem-before-hooks","The Logic-Reuse Problem Before Hooks",[15,16,17,18,22,23,26],"p",{},"Before React 16.8 introduced hooks, sharing stateful logic between components was genuinely hard. You had three options: copy-paste the logic, lift it into a common ancestor (coupling unrelated components), or use one of two composition patterns — ",[19,20,21],"strong",{},"render props"," or ",[19,24,25],{},"Higher-Order Components (HOCs)",". Both patterns solved the problem. Both left a legacy in every React codebase written before 2019, and interviewers still ask about them because they appear in existing code and because understanding them deepens your grasp of React's component model.",[10,28,30],{"id":29},"render-props","Render Props",[15,32,33,34,37],{},"A ",[19,35,36],{},"render prop"," is a prop whose value is a function. The component that owns the state calls that function instead of rendering JSX directly, passing the state as arguments. The caller decides what to render.",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-jsx shiki shiki-themes github-light github-dark","function DataFetcher({ url, render }) {\n  const [data, setData] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false); });\n  }, [url]);\n\n  return render({ data, loading }); \u002F\u002F state flows out, rendering is delegated\n}\n\n\u003CDataFetcher\n  url=\"\u002Fapi\u002Fusers\"\n  render={({ data, loading }) =>\n    loading ? \u003CSpinner \u002F> : \u003CUserList users={data} \u002F>\n  }\n\u002F>\n","jsx","",[46,47,48,78,114,142,149,164,220,226,231,247,253,258,267,279,302,336,342],"code",{"__ignoreMap":44},[49,50,53,57,61,65,69,72,75],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"szBVR","function",[49,58,60],{"class":59},"sScJk"," DataFetcher",[49,62,64],{"class":63},"sVt8B","({ ",[49,66,68],{"class":67},"s4XuR","url",[49,70,71],{"class":63},", ",[49,73,74],{"class":67},"render",[49,76,77],{"class":63}," }) {\n",[49,79,81,84,87,91,93,96,99,102,105,108,111],{"class":51,"line":80},2,[49,82,83],{"class":55},"  const",[49,85,86],{"class":63}," [",[49,88,90],{"class":89},"sj4cs","data",[49,92,71],{"class":63},[49,94,95],{"class":89},"setData",[49,97,98],{"class":63},"] ",[49,100,101],{"class":55},"=",[49,103,104],{"class":59}," useState",[49,106,107],{"class":63},"(",[49,109,110],{"class":89},"null",[49,112,113],{"class":63},");\n",[49,115,117,119,121,124,126,129,131,133,135,137,140],{"class":51,"line":116},3,[49,118,83],{"class":55},[49,120,86],{"class":63},[49,122,123],{"class":89},"loading",[49,125,71],{"class":63},[49,127,128],{"class":89},"setLoading",[49,130,98],{"class":63},[49,132,101],{"class":55},[49,134,104],{"class":59},[49,136,107],{"class":63},[49,138,139],{"class":89},"true",[49,141,113],{"class":63},[49,143,145],{"class":51,"line":144},4,[49,146,148],{"emptyLinePlaceholder":147},true,"\n",[49,150,152,155,158,161],{"class":51,"line":151},5,[49,153,154],{"class":59},"  useEffect",[49,156,157],{"class":63},"(() ",[49,159,160],{"class":55},"=>",[49,162,163],{"class":63}," {\n",[49,165,167,170,173,176,178,181,184,187,190,193,195,197,200,202,205,207,210,212,214,217],{"class":51,"line":166},6,[49,168,169],{"class":59},"    fetch",[49,171,172],{"class":63},"(url).",[49,174,175],{"class":59},"then",[49,177,107],{"class":63},[49,179,180],{"class":67},"r",[49,182,183],{"class":55}," =>",[49,185,186],{"class":63}," r.",[49,188,189],{"class":59},"json",[49,191,192],{"class":63},"()).",[49,194,175],{"class":59},[49,196,107],{"class":63},[49,198,199],{"class":67},"d",[49,201,183],{"class":55},[49,203,204],{"class":63}," { ",[49,206,95],{"class":59},[49,208,209],{"class":63},"(d); ",[49,211,128],{"class":59},[49,213,107],{"class":63},[49,215,216],{"class":89},"false",[49,218,219],{"class":63},"); });\n",[49,221,223],{"class":51,"line":222},7,[49,224,225],{"class":63},"  }, [url]);\n",[49,227,229],{"class":51,"line":228},8,[49,230,148],{"emptyLinePlaceholder":147},[49,232,234,237,240,243],{"class":51,"line":233},9,[49,235,236],{"class":55},"  return",[49,238,239],{"class":59}," render",[49,241,242],{"class":63},"({ data, loading }); ",[49,244,246],{"class":245},"sJ8bj","\u002F\u002F state flows out, rendering is delegated\n",[49,248,250],{"class":51,"line":249},10,[49,251,252],{"class":63},"}\n",[49,254,256],{"class":51,"line":255},11,[49,257,148],{"emptyLinePlaceholder":147},[49,259,261,264],{"class":51,"line":260},12,[49,262,263],{"class":63},"\u003C",[49,265,266],{"class":89},"DataFetcher\n",[49,268,270,273,275],{"class":51,"line":269},13,[49,271,272],{"class":59},"  url",[49,274,101],{"class":55},[49,276,278],{"class":277},"sZZnC","\"\u002Fapi\u002Fusers\"\n",[49,280,282,285,287,290,292,294,296,299],{"class":51,"line":281},14,[49,283,284],{"class":59},"  render",[49,286,101],{"class":55},[49,288,289],{"class":63},"{({ ",[49,291,90],{"class":67},[49,293,71],{"class":63},[49,295,123],{"class":67},[49,297,298],{"class":63}," }) ",[49,300,301],{"class":55},"=>\n",[49,303,305,308,311,314,317,320,323,325,328,331,333],{"class":51,"line":304},15,[49,306,307],{"class":63},"    loading ",[49,309,310],{"class":55},"?",[49,312,313],{"class":63}," \u003C",[49,315,316],{"class":89},"Spinner",[49,318,319],{"class":63}," \u002F> ",[49,321,322],{"class":55},":",[49,324,313],{"class":63},[49,326,327],{"class":89},"UserList",[49,329,330],{"class":59}," users",[49,332,101],{"class":55},[49,334,335],{"class":63},"{data} \u002F>\n",[49,337,339],{"class":51,"line":338},16,[49,340,341],{"class":63},"  }\n",[49,343,345],{"class":51,"line":344},17,[49,346,347],{"class":63},"\u002F>\n",[349,350,352],"h3",{"id":351},"function-as-child","Function-as-Child",[15,354,355,356,359,360,362,363,365,366,369],{},"When the render prop is named ",[46,357,358],{},"children"," instead of ",[46,361,74],{},", the pattern is called ",[19,364,351],{}," (or \"children as a function\"). Behaviour is identical — the component calls ",[46,367,368],{},"props.children(state)",". The syntax difference is cosmetic: the function lives inside the JSX tag body, which can read more naturally.",[39,371,373],{"className":41,"code":372,"language":43,"meta":44,"style":44},"\u003CMouse>\n  {({ x, y }) => \u003CCursor x={x} y={y} \u002F>}\n\u003C\u002FMouse>\n",[46,374,375,385,422],{"__ignoreMap":44},[49,376,377,379,382],{"class":51,"line":52},[49,378,263],{"class":63},[49,380,381],{"class":89},"Mouse",[49,383,384],{"class":63},">\n",[49,386,387,390,393,395,398,400,402,404,407,410,412,415,417,419],{"class":51,"line":80},[49,388,389],{"class":63},"  {({ ",[49,391,392],{"class":67},"x",[49,394,71],{"class":63},[49,396,397],{"class":67},"y",[49,399,298],{"class":63},[49,401,160],{"class":55},[49,403,313],{"class":63},[49,405,406],{"class":89},"Cursor",[49,408,409],{"class":59}," x",[49,411,101],{"class":55},[49,413,414],{"class":63},"{x} ",[49,416,397],{"class":59},[49,418,101],{"class":55},[49,420,421],{"class":63},"{y} \u002F>}\n",[49,423,424,427,429],{"class":51,"line":116},[49,425,426],{"class":63},"\u003C\u002F",[49,428,381],{"class":89},[49,430,384],{"class":63},[15,432,433,434,436,437,71,440,443,444,446],{},"Use an explicit ",[46,435,74],{}," prop when you need multiple render slots (",[46,438,439],{},"renderHeader",[46,441,442],{},"renderFooter","). Use ",[46,445,358],{}," when there is only one slot.",[10,448,450],{"id":449},"higher-order-components","Higher-Order Components",[15,452,33,453,456,457,460],{},[19,454,455],{},"Higher-Order Component"," is a function that takes a component and returns a new, enhanced component — the React adaptation of the higher-order function concept. The canonical naming convention is ",[46,458,459],{},"withXxx"," (camelCase, \"with\" prefix).",[39,462,466],{"className":463,"code":464,"language":465,"meta":44,"style":44},"language-ts shiki shiki-themes github-light github-dark","function withAuth\u003CP extends WithAuthProps>(\n  WrappedComponent: React.ComponentType\u003CP>\n): React.ComponentType\u003COmit\u003CP, keyof WithAuthProps>> {\n\n  function WithAuth(props: Omit\u003CP, keyof WithAuthProps>) {\n    const { user } = useAuth();\n    if (!user) return \u003CNavigate to=\"\u002Flogin\" replace \u002F>;\n    return \u003CWrappedComponent {...(props as P)} currentUser={user} \u002F>;\n  }\n\n  WithAuth.displayName =\n    `WithAuth(${WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'})`;\n\n  return WithAuth;\n}\n","ts",[46,467,468,489,511,543,547,578,599,635,675,679,683,691,722,726,733],{"__ignoreMap":44},[49,469,470,472,475,477,480,483,486],{"class":51,"line":52},[49,471,56],{"class":55},[49,473,474],{"class":59}," withAuth",[49,476,263],{"class":63},[49,478,479],{"class":59},"P",[49,481,482],{"class":55}," extends",[49,484,485],{"class":59}," WithAuthProps",[49,487,488],{"class":63},">(\n",[49,490,491,494,496,499,502,505,507,509],{"class":51,"line":80},[49,492,493],{"class":67},"  WrappedComponent",[49,495,322],{"class":55},[49,497,498],{"class":59}," React",[49,500,501],{"class":63},".",[49,503,504],{"class":59},"ComponentType",[49,506,263],{"class":63},[49,508,479],{"class":59},[49,510,384],{"class":63},[49,512,513,516,518,520,522,524,526,529,531,533,535,538,540],{"class":51,"line":116},[49,514,515],{"class":63},")",[49,517,322],{"class":55},[49,519,498],{"class":59},[49,521,501],{"class":63},[49,523,504],{"class":59},[49,525,263],{"class":63},[49,527,528],{"class":59},"Omit",[49,530,263],{"class":63},[49,532,479],{"class":59},[49,534,71],{"class":63},[49,536,537],{"class":55},"keyof",[49,539,485],{"class":59},[49,541,542],{"class":63},">> {\n",[49,544,545],{"class":51,"line":144},[49,546,148],{"emptyLinePlaceholder":147},[49,548,549,552,555,557,560,562,565,567,569,571,573,575],{"class":51,"line":151},[49,550,551],{"class":55},"  function",[49,553,554],{"class":59}," WithAuth",[49,556,107],{"class":63},[49,558,559],{"class":67},"props",[49,561,322],{"class":55},[49,563,564],{"class":59}," Omit",[49,566,263],{"class":63},[49,568,479],{"class":59},[49,570,71],{"class":63},[49,572,537],{"class":55},[49,574,485],{"class":59},[49,576,577],{"class":63},">) {\n",[49,579,580,583,585,588,591,593,596],{"class":51,"line":166},[49,581,582],{"class":55},"    const",[49,584,204],{"class":63},[49,586,587],{"class":89},"user",[49,589,590],{"class":63}," } ",[49,592,101],{"class":55},[49,594,595],{"class":59}," useAuth",[49,597,598],{"class":63},"();\n",[49,600,601,604,607,610,613,616,618,621,624,626,629,632],{"class":51,"line":222},[49,602,603],{"class":55},"    if",[49,605,606],{"class":63}," (",[49,608,609],{"class":55},"!",[49,611,612],{"class":63},"user) ",[49,614,615],{"class":55},"return",[49,617,313],{"class":63},[49,619,620],{"class":59},"Navigate",[49,622,623],{"class":59}," to",[49,625,101],{"class":63},[49,627,628],{"class":277},"\"\u002Flogin\"",[49,630,631],{"class":59}," replace",[49,633,634],{"class":63}," \u002F>;\n",[49,636,637,640,642,645,648,651,653,655,658,661,664,667,670,672],{"class":51,"line":228},[49,638,639],{"class":55},"    return",[49,641,313],{"class":63},[49,643,644],{"class":59},"WrappedComponent",[49,646,647],{"class":63}," {",[49,649,650],{"class":55},"...",[49,652,107],{"class":63},[49,654,559],{"class":59},[49,656,657],{"class":59}," as",[49,659,660],{"class":59}," P",[49,662,663],{"class":63},")} ",[49,665,666],{"class":59},"currentUser",[49,668,669],{"class":63},"={",[49,671,587],{"class":67},[49,673,674],{"class":63},"} \u002F>;\n",[49,676,677],{"class":51,"line":233},[49,678,341],{"class":63},[49,680,681],{"class":51,"line":249},[49,682,148],{"emptyLinePlaceholder":147},[49,684,685,688],{"class":51,"line":255},[49,686,687],{"class":63},"  WithAuth.displayName ",[49,689,690],{"class":55},"=\n",[49,692,693,696,698,700,703,706,709,711,714,716,719],{"class":51,"line":260},[49,694,695],{"class":277},"    `WithAuth(${",[49,697,644],{"class":63},[49,699,501],{"class":277},[49,701,702],{"class":63},"displayName",[49,704,705],{"class":55}," ??",[49,707,708],{"class":63}," WrappedComponent",[49,710,501],{"class":277},[49,712,713],{"class":63},"name",[49,715,705],{"class":55},[49,717,718],{"class":277}," 'Component'})`",[49,720,721],{"class":63},";\n",[49,723,724],{"class":51,"line":269},[49,725,148],{"emptyLinePlaceholder":147},[49,727,728,730],{"class":51,"line":281},[49,729,236],{"class":55},[49,731,732],{"class":63}," WithAuth;\n",[49,734,735],{"class":51,"line":304},[49,736,252],{"class":63},[15,738,739,740,742,743,746],{},"Always set ",[46,741,702],{}," in the ",[46,744,745],{},"WithXxx(InnerName)"," format. Without it, React DevTools shows anonymous components and stack traces become unreadable.",[10,748,750],{"id":749},"cross-cutting-concerns","Cross-Cutting Concerns",[15,752,753,754,757],{},"HOCs excel at ",[19,755,756],{},"cross-cutting concerns"," — behaviour that applies uniformly across many components without those components needing to know about it:",[759,760,761,768,774],"ul",{},[762,763,764,767],"li",{},[19,765,766],{},"Auth guards"," — redirect unauthenticated users before rendering a page",[762,769,770,773],{},[19,771,772],{},"Analytics logging"," — record component mount\u002Funmount without touching business logic",[762,775,776,779],{},[19,777,778],{},"Error boundaries"," — wrap any component with standardised error UI",[15,781,782],{},"The consumer's code stays clean; the HOC handles the concern invisibly.",[10,784,786],{"id":785},"prop-collision","Prop Collision",[15,788,789,790,793],{},"HOC-injected props can silently overwrite props that parents or consumers supply with the same name. The fix is using ",[19,791,792],{},"namespaced prop names"," and making the contract explicit in TypeScript:",[39,795,797],{"className":41,"code":796,"language":43,"meta":44,"style":44},"\u002F\u002F BAD — HOC injects \"user\" which could clash with a parent's \"user\" prop\nfunction withUser(Wrapped) {\n  return props => \u003CWrapped {...props} user={getCurrentUser()} \u002F>;\n}\n\n\u002F\u002F GOOD — descriptive namespaced name avoids collisions\nfunction withUser(Wrapped) {\n  return props => \u003CWrapped {...props} currentUser={getCurrentUser()} \u002F>;\n}\n",[46,798,799,804,819,852,856,860,865,877,905],{"__ignoreMap":44},[49,800,801],{"class":51,"line":52},[49,802,803],{"class":245},"\u002F\u002F BAD — HOC injects \"user\" which could clash with a parent's \"user\" prop\n",[49,805,806,808,811,813,816],{"class":51,"line":80},[49,807,56],{"class":55},[49,809,810],{"class":59}," withUser",[49,812,107],{"class":63},[49,814,815],{"class":67},"Wrapped",[49,817,818],{"class":63},") {\n",[49,820,821,823,826,828,830,832,834,836,839,841,843,846,849],{"class":51,"line":116},[49,822,236],{"class":55},[49,824,825],{"class":67}," props",[49,827,183],{"class":55},[49,829,313],{"class":63},[49,831,815],{"class":89},[49,833,647],{"class":63},[49,835,650],{"class":55},[49,837,838],{"class":63},"props} ",[49,840,587],{"class":59},[49,842,101],{"class":55},[49,844,845],{"class":63},"{",[49,847,848],{"class":59},"getCurrentUser",[49,850,851],{"class":63},"()} \u002F>;\n",[49,853,854],{"class":51,"line":144},[49,855,252],{"class":63},[49,857,858],{"class":51,"line":151},[49,859,148],{"emptyLinePlaceholder":147},[49,861,862],{"class":51,"line":166},[49,863,864],{"class":245},"\u002F\u002F GOOD — descriptive namespaced name avoids collisions\n",[49,866,867,869,871,873,875],{"class":51,"line":222},[49,868,56],{"class":55},[49,870,810],{"class":59},[49,872,107],{"class":63},[49,874,815],{"class":67},[49,876,818],{"class":63},[49,878,879,881,883,885,887,889,891,893,895,897,899,901,903],{"class":51,"line":228},[49,880,236],{"class":55},[49,882,825],{"class":67},[49,884,183],{"class":55},[49,886,313],{"class":63},[49,888,815],{"class":89},[49,890,647],{"class":63},[49,892,650],{"class":55},[49,894,838],{"class":63},[49,896,666],{"class":59},[49,898,101],{"class":55},[49,900,845],{"class":63},[49,902,848],{"class":59},[49,904,851],{"class":63},[49,906,907],{"class":51,"line":233},[49,908,252],{"class":63},[10,910,912],{"id":911},"hoc-composition","HOC Composition",[15,914,915,916,919],{},"Stacking multiple HOCs with nested calls reads inside-out and becomes unmaintainable. A ",[46,917,918],{},"compose"," utility applies functions right-to-left, making the reading order match the wrapping order:",[39,921,925],{"className":922,"code":923,"language":924,"meta":44,"style":44},"language-js shiki shiki-themes github-light github-dark","import { compose } from 'redux'; \u002F\u002F or any compose utility\n\nconst enhance = compose(\n  withLogger,   \u002F\u002F applied last (outermost)\n  withAuth,     \u002F\u002F applied second\n  withTheme,    \u002F\u002F applied first (innermost)\n);\n\nexport default enhance(MyComponent);\n","js",[46,926,927,947,951,968,976,984,992,996,1000],{"__ignoreMap":44},[49,928,929,932,935,938,941,944],{"class":51,"line":52},[49,930,931],{"class":55},"import",[49,933,934],{"class":63}," { compose } ",[49,936,937],{"class":55},"from",[49,939,940],{"class":277}," 'redux'",[49,942,943],{"class":63},"; ",[49,945,946],{"class":245},"\u002F\u002F or any compose utility\n",[49,948,949],{"class":51,"line":80},[49,950,148],{"emptyLinePlaceholder":147},[49,952,953,956,959,962,965],{"class":51,"line":116},[49,954,955],{"class":55},"const",[49,957,958],{"class":89}," enhance",[49,960,961],{"class":55}," =",[49,963,964],{"class":59}," compose",[49,966,967],{"class":63},"(\n",[49,969,970,973],{"class":51,"line":144},[49,971,972],{"class":63},"  withLogger,   ",[49,974,975],{"class":245},"\u002F\u002F applied last (outermost)\n",[49,977,978,981],{"class":51,"line":151},[49,979,980],{"class":63},"  withAuth,     ",[49,982,983],{"class":245},"\u002F\u002F applied second\n",[49,985,986,989],{"class":51,"line":166},[49,987,988],{"class":63},"  withTheme,    ",[49,990,991],{"class":245},"\u002F\u002F applied first (innermost)\n",[49,993,994],{"class":51,"line":222},[49,995,113],{"class":63},[49,997,998],{"class":51,"line":228},[49,999,148],{"emptyLinePlaceholder":147},[49,1001,1002,1005,1008,1010],{"class":51,"line":233},[49,1003,1004],{"class":55},"export",[49,1006,1007],{"class":55}," default",[49,1009,958],{"class":59},[49,1011,1012],{"class":63},"(MyComponent);\n",[15,1014,1015,1016,1019,1020,1023,1024,1027,1028,1030,1031,1033],{},"Order matters: the outermost HOC receives props first. If ",[46,1017,1018],{},"withAuth"," reads a ",[46,1021,1022],{},"theme"," prop provided by ",[46,1025,1026],{},"withTheme",", then ",[46,1029,1026],{}," must be listed after ",[46,1032,1018],{}," in the compose call.",[10,1035,1037],{"id":1036},"wrapper-hell","Wrapper Hell",[15,1039,1040,1041,1044],{},"In React DevTools, heavily HOC-wrapped components show as nested layers: ",[46,1042,1043],{},"WithRouter > WithTheme > WithAuth > WithLogger > Dashboard",". This \"wrapper hell\" makes the component tree hard to navigate and error stacks hard to read. The solution is collapsing HOC stacks into custom hooks — which became the standard approach after React 16.8.",[10,1046,1048],{"id":1047},"why-hooks-replaced-them","Why Hooks Replaced Them",[15,1050,1051],{},"Custom hooks solve the same logic-reuse problem with none of the drawbacks:",[1053,1054,1055,1071],"table",{},[1056,1057,1058],"thead",{},[1059,1060,1061,1065,1068],"tr",{},[1062,1063,1064],"th",{},"Issue",[1062,1066,1067],{},"Render props \u002F HOCs",[1062,1069,1070],{},"Custom hooks",[1072,1073,1074,1088,1101,1114],"tbody",{},[1059,1075,1076,1082,1085],{},[1077,1078,1079],"td",{},[19,1080,1081],{},"Extra DOM nodes",[1077,1083,1084],{},"Yes — each adds a wrapper component",[1077,1086,1087],{},"No — hooks run inside the calling component",[1059,1089,1090,1095,1098],{},[1077,1091,1092],{},[19,1093,1094],{},"Prop collision",[1077,1096,1097],{},"HOCs inject into the prop namespace",[1077,1099,1100],{},"Hooks return values via destructuring",[1059,1102,1103,1108,1111],{},[1077,1104,1105],{},[19,1106,1107],{},"DevTools clarity",[1077,1109,1110],{},"Deep wrapper tree",[1077,1112,1113],{},"Flat component tree",[1059,1115,1116,1121,1124],{},[1077,1117,1118],{},[19,1119,1120],{},"TypeScript",[1077,1122,1123],{},"Generics + conditional types",[1077,1125,1126],{},"Plain function return types",[39,1128,1130],{"className":41,"code":1129,"language":43,"meta":44,"style":44},"\u002F\u002F Pre-hooks: render prop for mouse position\n\u003CMouseTracker render={({ x, y }) => \u003CCrosshair x={x} y={y} \u002F>} \u002F>\n\n\u002F\u002F Post-hooks: custom hook — same logic, no wrapper component\nfunction useMousePosition() {\n  const [pos, setPos] = useState({ x: 0, y: 0 });\n  useEffect(() => {\n    const handler = e => setPos({ x: e.clientX, y: e.clientY });\n    window.addEventListener('mousemove', handler);\n    return () => window.removeEventListener('mousemove', handler);\n  }, []);\n  return pos;\n}\n",[46,1131,1132,1137,1178,1182,1187,1197,1231,1241,1261,1277,1298,1303,1310],{"__ignoreMap":44},[49,1133,1134],{"class":51,"line":52},[49,1135,1136],{"class":245},"\u002F\u002F Pre-hooks: render prop for mouse position\n",[49,1138,1139,1141,1144,1146,1148,1150,1152,1154,1156,1158,1160,1162,1165,1167,1169,1171,1173,1175],{"class":51,"line":80},[49,1140,263],{"class":63},[49,1142,1143],{"class":89},"MouseTracker",[49,1145,239],{"class":59},[49,1147,101],{"class":55},[49,1149,289],{"class":63},[49,1151,392],{"class":67},[49,1153,71],{"class":63},[49,1155,397],{"class":67},[49,1157,298],{"class":63},[49,1159,160],{"class":55},[49,1161,313],{"class":63},[49,1163,1164],{"class":89},"Crosshair",[49,1166,409],{"class":59},[49,1168,101],{"class":55},[49,1170,414],{"class":63},[49,1172,397],{"class":59},[49,1174,101],{"class":55},[49,1176,1177],{"class":63},"{y} \u002F>} \u002F>\n",[49,1179,1180],{"class":51,"line":116},[49,1181,148],{"emptyLinePlaceholder":147},[49,1183,1184],{"class":51,"line":144},[49,1185,1186],{"class":245},"\u002F\u002F Post-hooks: custom hook — same logic, no wrapper component\n",[49,1188,1189,1191,1194],{"class":51,"line":151},[49,1190,56],{"class":55},[49,1192,1193],{"class":59}," useMousePosition",[49,1195,1196],{"class":63},"() {\n",[49,1198,1199,1201,1203,1206,1208,1211,1213,1215,1217,1220,1223,1226,1228],{"class":51,"line":166},[49,1200,83],{"class":55},[49,1202,86],{"class":63},[49,1204,1205],{"class":89},"pos",[49,1207,71],{"class":63},[49,1209,1210],{"class":89},"setPos",[49,1212,98],{"class":63},[49,1214,101],{"class":55},[49,1216,104],{"class":59},[49,1218,1219],{"class":63},"({ x: ",[49,1221,1222],{"class":89},"0",[49,1224,1225],{"class":63},", y: ",[49,1227,1222],{"class":89},[49,1229,1230],{"class":63}," });\n",[49,1232,1233,1235,1237,1239],{"class":51,"line":222},[49,1234,154],{"class":59},[49,1236,157],{"class":63},[49,1238,160],{"class":55},[49,1240,163],{"class":63},[49,1242,1243,1245,1248,1250,1253,1255,1258],{"class":51,"line":228},[49,1244,582],{"class":55},[49,1246,1247],{"class":59}," handler",[49,1249,961],{"class":55},[49,1251,1252],{"class":67}," e",[49,1254,183],{"class":55},[49,1256,1257],{"class":59}," setPos",[49,1259,1260],{"class":63},"({ x: e.clientX, y: e.clientY });\n",[49,1262,1263,1266,1269,1271,1274],{"class":51,"line":233},[49,1264,1265],{"class":63},"    window.",[49,1267,1268],{"class":59},"addEventListener",[49,1270,107],{"class":63},[49,1272,1273],{"class":277},"'mousemove'",[49,1275,1276],{"class":63},", handler);\n",[49,1278,1279,1281,1284,1286,1289,1292,1294,1296],{"class":51,"line":249},[49,1280,639],{"class":55},[49,1282,1283],{"class":63}," () ",[49,1285,160],{"class":55},[49,1287,1288],{"class":63}," window.",[49,1290,1291],{"class":59},"removeEventListener",[49,1293,107],{"class":63},[49,1295,1273],{"class":277},[49,1297,1276],{"class":63},[49,1299,1300],{"class":51,"line":255},[49,1301,1302],{"class":63},"  }, []);\n",[49,1304,1305,1307],{"class":51,"line":260},[49,1306,236],{"class":55},[49,1308,1309],{"class":63}," pos;\n",[49,1311,1312],{"class":51,"line":269},[49,1313,252],{"class":63},[10,1315,1317],{"id":1316},"when-theyre-still-the-right-tool","When They're Still the Right Tool",[15,1319,1320],{},"Despite hooks, render props and HOCs are still relevant in specific cases:",[759,1322,1323,1337,1343],{},[762,1324,1325,1328,1329,1332,1333,1336],{},[19,1326,1327],{},"Render delegation to a library"," — ",[46,1330,1331],{},"react-window","'s ",[46,1334,1335],{},"\u003CFixedSizeList>"," calls a render prop for each row because the library controls the timing and injected style. Hooks cannot replace this.",[762,1338,1339,1342],{},[19,1340,1341],{},"Class component consumers"," — hooks cannot run inside class components. An HOC is the bridge that feeds hook-based logic into a class component.",[762,1344,1345,1348],{},[19,1346,1347],{},"Transparent interception"," — HOCs can decorate a component without the consumer noticing, which is the right model for analytics or logging.",[10,1350,1352],{"id":1351},"static-methods-and-forwardref","Static Methods and forwardRef",[15,1354,1355],{},"Two common pitfalls with HOCs that interviewers probe:",[15,1357,1358,1361,1362,1365,1366,501],{},[19,1359,1360],{},"Static methods are lost."," The HOC returns a new function object; static methods from the original (like Next.js ",[46,1363,1364],{},"getLayout",") do not copy over automatically. Fix: ",[46,1367,1368],{},"hoistNonReactStatics(WithAuth, WrappedComponent)",[15,1370,1371,1374,1375,1378,1379,1382,1383,1385],{},[19,1372,1373],{},"Refs do not forward."," ",[46,1376,1377],{},"ref"," is not a real prop — it attaches to the wrapper, not the wrapped component. Fix: wrap the HOC body in ",[46,1380,1381],{},"React.forwardRef"," and thread the ",[46,1384,1377],{}," argument through:",[39,1387,1389],{"className":41,"code":1388,"language":43,"meta":44,"style":44},"const WithFocusRing = React.forwardRef(function(props, ref) {\n  return \u003CWrappedComponent {...props} ref={ref} \u002F>;\n});\nhoistNonReactStatics(WithFocusRing, WrappedComponent);\n",[46,1390,1391,1420,1441,1446],{"__ignoreMap":44},[49,1392,1393,1395,1398,1400,1403,1406,1408,1410,1412,1414,1416,1418],{"class":51,"line":52},[49,1394,955],{"class":55},[49,1396,1397],{"class":89}," WithFocusRing",[49,1399,961],{"class":55},[49,1401,1402],{"class":63}," React.",[49,1404,1405],{"class":59},"forwardRef",[49,1407,107],{"class":63},[49,1409,56],{"class":55},[49,1411,107],{"class":63},[49,1413,559],{"class":67},[49,1415,71],{"class":63},[49,1417,1377],{"class":67},[49,1419,818],{"class":63},[49,1421,1422,1424,1426,1428,1430,1432,1434,1436,1438],{"class":51,"line":80},[49,1423,236],{"class":55},[49,1425,313],{"class":63},[49,1427,644],{"class":89},[49,1429,647],{"class":63},[49,1431,650],{"class":55},[49,1433,838],{"class":63},[49,1435,1377],{"class":59},[49,1437,101],{"class":55},[49,1439,1440],{"class":63},"{ref} \u002F>;\n",[49,1442,1443],{"class":51,"line":116},[49,1444,1445],{"class":63},"});\n",[49,1447,1448,1451],{"class":51,"line":144},[49,1449,1450],{"class":59},"hoistNonReactStatics",[49,1452,1453],{"class":63},"(WithFocusRing, WrappedComponent);\n",[10,1455,1457],{"id":1456},"performance-the-inline-render-prop-trap","Performance: The Inline Render Prop Trap",[15,1459,1460,1461,22,1464,1467],{},"Passing an inline arrow function as a render prop creates a new function reference on every parent render. When the render-prop component uses ",[46,1462,1463],{},"React.memo",[46,1465,1466],{},"PureComponent",", the shallow comparison always fails and the optimisation is bypassed.",[15,1469,1470,1471,1474],{},"Fix: stabilise the reference with ",[46,1472,1473],{},"useCallback"," (functional components) or a class instance method (class components):",[39,1476,1478],{"className":41,"code":1477,"language":43,"meta":44,"style":44},"\u002F\u002F BAD — new reference every render, memo is defeated\n\u003CMouse render={({ x, y }) => \u003CCrosshair x={x} y={y} \u002F>} \u002F>\n\n\u002F\u002F GOOD — stable reference\nconst renderCrosshair = useCallback(({ x, y }) => \u003CCrosshair x={x} y={y} \u002F>, []);\n\u003CMouse render={renderCrosshair} \u002F>\n",[46,1479,1480,1485,1523,1527,1532,1574],{"__ignoreMap":44},[49,1481,1482],{"class":51,"line":52},[49,1483,1484],{"class":245},"\u002F\u002F BAD — new reference every render, memo is defeated\n",[49,1486,1487,1489,1491,1493,1495,1497,1499,1501,1503,1505,1507,1509,1511,1513,1515,1517,1519,1521],{"class":51,"line":80},[49,1488,263],{"class":63},[49,1490,381],{"class":89},[49,1492,239],{"class":59},[49,1494,101],{"class":55},[49,1496,289],{"class":63},[49,1498,392],{"class":67},[49,1500,71],{"class":63},[49,1502,397],{"class":67},[49,1504,298],{"class":63},[49,1506,160],{"class":55},[49,1508,313],{"class":63},[49,1510,1164],{"class":89},[49,1512,409],{"class":59},[49,1514,101],{"class":55},[49,1516,414],{"class":63},[49,1518,397],{"class":59},[49,1520,101],{"class":55},[49,1522,1177],{"class":63},[49,1524,1525],{"class":51,"line":116},[49,1526,148],{"emptyLinePlaceholder":147},[49,1528,1529],{"class":51,"line":144},[49,1530,1531],{"class":245},"\u002F\u002F GOOD — stable reference\n",[49,1533,1534,1536,1539,1541,1544,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567,1569,1571],{"class":51,"line":151},[49,1535,955],{"class":55},[49,1537,1538],{"class":89}," renderCrosshair",[49,1540,961],{"class":55},[49,1542,1543],{"class":59}," useCallback",[49,1545,1546],{"class":63},"(({ ",[49,1548,392],{"class":67},[49,1550,71],{"class":63},[49,1552,397],{"class":67},[49,1554,298],{"class":63},[49,1556,160],{"class":55},[49,1558,313],{"class":63},[49,1560,1164],{"class":89},[49,1562,409],{"class":59},[49,1564,101],{"class":55},[49,1566,414],{"class":63},[49,1568,397],{"class":59},[49,1570,101],{"class":55},[49,1572,1573],{"class":63},"{y} \u002F>, []);\n",[49,1575,1576,1578,1580,1582,1584],{"class":51,"line":166},[49,1577,263],{"class":63},[49,1579,381],{"class":89},[49,1581,239],{"class":59},[49,1583,101],{"class":55},[49,1585,1586],{"class":63},"{renderCrosshair} \u002F>\n",[10,1588,1590],{"id":1589},"testing-strategy","Testing Strategy",[15,1592,1593,1594,1597],{},"For HOC-wrapped components, use the ",[19,1595,1596],{},"dual export pattern",": export the base component as a named export and the HOC-wrapped version as the default. Unit tests import the named export to test business logic in isolation; integration tests import the default to test the HOC's own behaviour.",[39,1599,1603],{"className":1600,"code":1601,"language":1602,"meta":44,"style":44},"language-tsx shiki shiki-themes github-light github-dark","export function UserList({ currentUser }) { \u002F* ... *\u002F }   \u002F\u002F named — no HOC\nexport default withAuth(UserList);                         \u002F\u002F default — wrapped\n\n\u002F\u002F Unit test — bypasses HOC\nimport { UserList } from '.\u002FUserList';\nrender(\u003CUserList currentUser={mockUser} \u002F>);\n","tsx",[46,1604,1605,1631,1645,1649,1654,1668],{"__ignoreMap":44},[49,1606,1607,1609,1612,1615,1617,1619,1622,1625,1628],{"class":51,"line":52},[49,1608,1004],{"class":55},[49,1610,1611],{"class":55}," function",[49,1613,1614],{"class":59}," UserList",[49,1616,64],{"class":63},[49,1618,666],{"class":67},[49,1620,1621],{"class":63}," }) { ",[49,1623,1624],{"class":245},"\u002F* ... *\u002F",[49,1626,1627],{"class":63}," }   ",[49,1629,1630],{"class":245},"\u002F\u002F named — no HOC\n",[49,1632,1633,1635,1637,1639,1642],{"class":51,"line":80},[49,1634,1004],{"class":55},[49,1636,1007],{"class":55},[49,1638,474],{"class":59},[49,1640,1641],{"class":63},"(UserList);                         ",[49,1643,1644],{"class":245},"\u002F\u002F default — wrapped\n",[49,1646,1647],{"class":51,"line":116},[49,1648,148],{"emptyLinePlaceholder":147},[49,1650,1651],{"class":51,"line":144},[49,1652,1653],{"class":245},"\u002F\u002F Unit test — bypasses HOC\n",[49,1655,1656,1658,1661,1663,1666],{"class":51,"line":151},[49,1657,931],{"class":55},[49,1659,1660],{"class":63}," { UserList } ",[49,1662,937],{"class":55},[49,1664,1665],{"class":277}," '.\u002FUserList'",[49,1667,721],{"class":63},[49,1669,1670,1672,1675,1677,1680,1682],{"class":51,"line":166},[49,1671,74],{"class":59},[49,1673,1674],{"class":63},"(\u003C",[49,1676,327],{"class":89},[49,1678,1679],{"class":59}," currentUser",[49,1681,101],{"class":55},[49,1683,1684],{"class":63},"{mockUser} \u002F>);\n",[10,1686,1688],{"id":1687},"key-takeaways","Key Takeaways",[759,1690,1691,1696,1705,1717,1723,1731,1737,1740,1749],{},[762,1692,33,1693,1695],{},[19,1694,36],{}," delegates rendering by calling a function prop with shared state — the caller decides the UI.",[762,1697,1698,1701,1702,1704],{},[19,1699,1700],{},"Function-as-child"," is a render prop where the prop name is ",[46,1703,358],{},"; the mechanics are identical.",[762,1706,1707,1708,1711,1712,1714,1715,501],{},"An ",[19,1709,1710],{},"HOC"," is a function from component to component; name it ",[46,1713,459],{}," and always set ",[46,1716,702],{},[762,1718,1719,1720,1722],{},"HOCs are ideal for ",[19,1721,756],{}," (auth guards, logging) that should be invisible to consumers.",[762,1724,1725,1727,1728,501],{},[19,1726,1094],{}," is avoided with namespaced injection prop names and TypeScript ",[46,1729,1730],{},"Omit\u003CP, keyof InjectedProps>",[762,1732,1733,1734,1736],{},"Compose HOCs with a ",[46,1735,918],{}," utility to avoid inside-out nesting.",[762,1738,1739],{},"Custom hooks replaced both patterns for most new code — but render props are still the right API when a library needs a JSX factory, and HOCs remain the only bridge into class components.",[762,1741,1742,1743,1745,1746,1748],{},"Always use ",[46,1744,1450],{}," and ",[46,1747,1381],{}," together in every HOC.",[762,1750,1751,1752,1754],{},"Stabilise render prop function references with ",[46,1753,1473],{}," to avoid defeating memoisation.",[1756,1757,1758],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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":44,"searchDepth":80,"depth":80,"links":1760},[1761,1762,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775],{"id":12,"depth":80,"text":13},{"id":29,"depth":80,"text":30,"children":1763},[1764],{"id":351,"depth":116,"text":352},{"id":449,"depth":80,"text":450},{"id":749,"depth":80,"text":750},{"id":785,"depth":80,"text":786},{"id":911,"depth":80,"text":912},{"id":1036,"depth":80,"text":1037},{"id":1047,"depth":80,"text":1048},{"id":1316,"depth":80,"text":1317},{"id":1351,"depth":80,"text":1352},{"id":1456,"depth":80,"text":1457},{"id":1589,"depth":80,"text":1590},{"id":1687,"depth":80,"text":1688},"Master React render props and HOCs for interviews — function-as-child, higher-order components, cross-cutting concerns, prop collision, hooks vs HOCs, and TypeScript generics.","medium","md","React","react",{},"\u002Fblog\u002Freact-render-props-hoc-guide","\u002Freact\u002Fpatterns\u002Frender-props-hoc",{"title":5,"description":1776},"blog\u002Freact-render-props-hoc-guide","Render Props & HOCs","Patterns","patterns","2026-06-24","2yDBqr4r3HDvyDB3x2rhM0pWp_Z5ioF29No3ccyziz4",1782244083220]