[{"data":1,"prerenderedAt":2581},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-custom-hooks-guide":3},{"id":4,"title":5,"body":6,"description":2566,"difficulty":2567,"extension":2568,"framework":2569,"frameworkSlug":2570,"meta":2571,"navigation":111,"order":171,"path":2572,"qaPath":2573,"seo":2574,"stem":2575,"subtopic":2576,"topic":2577,"topicSlug":2578,"updated":2579,"__hash__":2580},"blog\u002Fblog\u002Freact-custom-hooks-guide.md","React Custom Hooks — Complete Interview Guide with Examples",{"type":7,"value":8,"toc":2545},"minimark",[9,14,22,34,38,49,245,263,270,280,385,388,392,404,531,534,538,541,615,734,737,741,746,749,843,846,849,1006,1015,1019,1029,1258,1268,1272,1275,1475,1481,1485,1488,1773,1776,1780,1783,1875,1878,1882,1885,1915,1918,1922,1925,1955,2024,2027,2031,2045,2261,2267,2286,2290,2293,2371,2374,2485,2489,2541],[10,11,13],"h2",{"id":12},"why-react-custom-hooks-matter-in-interviews","Why React custom hooks matter in interviews",[15,16,17,21],"p",{},[18,19,20],"strong",{},"React custom hooks"," are one of the first things senior interviewers ask about because\nthey reveal how well you understand the hooks model at a conceptual level — not just the\nbuilt-in hooks, but why they exist and how to compose them into reusable patterns.",[15,23,24,25,29,30,33],{},"The question usually starts simple (\"What is a custom hook?\") and escalates quickly:\n\"Write a ",[26,27,28],"code",{},"useDebounce"," hook.\" \"How would you test it?\" \"Why is naming the function\n",[26,31,32],{},"useX"," important?\" \"How does it differ from a higher-order component?\" These questions\nall probe whether you've internalized hooks as a composition primitive, not just an API\nto memorize.",[10,35,37],{"id":36},"what-a-custom-hook-actually-is","What a custom hook actually is",[15,39,40,41,44,45,48],{},"A custom hook is a ",[18,42,43],{},"plain JavaScript function"," whose name starts with ",[26,46,47],{},"use"," and that\ncalls at least one React hook internally. Nothing more. No special API, no class, no\nregistration — just a function that follows the rules of hooks.",[50,51,56],"pre",{"className":52,"code":53,"language":54,"meta":55,"style":55},"language-jsx shiki shiki-themes github-light github-dark","function useWindowWidth() {\n  const [width, setWidth] = useState(window.innerWidth)\n\n  useEffect(() => {\n    const handler = () => setWidth(window.innerWidth)\n    window.addEventListener('resize', handler)\n    return () => window.removeEventListener('resize', handler)\n  }, [])\n\n  return width\n}\n\n\u002F\u002F any component can use it\nconst width = useWindowWidth()\n","jsx","",[26,57,58,75,106,113,128,150,169,191,197,202,211,217,222,229],{"__ignoreMap":55},[59,60,63,67,71],"span",{"class":61,"line":62},"line",1,[59,64,66],{"class":65},"szBVR","function",[59,68,70],{"class":69},"sScJk"," useWindowWidth",[59,72,74],{"class":73},"sVt8B","() {\n",[59,76,78,81,84,88,91,94,97,100,103],{"class":61,"line":77},2,[59,79,80],{"class":65},"  const",[59,82,83],{"class":73}," [",[59,85,87],{"class":86},"sj4cs","width",[59,89,90],{"class":73},", ",[59,92,93],{"class":86},"setWidth",[59,95,96],{"class":73},"] ",[59,98,99],{"class":65},"=",[59,101,102],{"class":69}," useState",[59,104,105],{"class":73},"(window.innerWidth)\n",[59,107,109],{"class":61,"line":108},3,[59,110,112],{"emptyLinePlaceholder":111},true,"\n",[59,114,116,119,122,125],{"class":61,"line":115},4,[59,117,118],{"class":69},"  useEffect",[59,120,121],{"class":73},"(() ",[59,123,124],{"class":65},"=>",[59,126,127],{"class":73}," {\n",[59,129,131,134,137,140,143,145,148],{"class":61,"line":130},5,[59,132,133],{"class":65},"    const",[59,135,136],{"class":69}," handler",[59,138,139],{"class":65}," =",[59,141,142],{"class":73}," () ",[59,144,124],{"class":65},[59,146,147],{"class":69}," setWidth",[59,149,105],{"class":73},[59,151,153,156,159,162,166],{"class":61,"line":152},6,[59,154,155],{"class":73},"    window.",[59,157,158],{"class":69},"addEventListener",[59,160,161],{"class":73},"(",[59,163,165],{"class":164},"sZZnC","'resize'",[59,167,168],{"class":73},", handler)\n",[59,170,172,175,177,179,182,185,187,189],{"class":61,"line":171},7,[59,173,174],{"class":65},"    return",[59,176,142],{"class":73},[59,178,124],{"class":65},[59,180,181],{"class":73}," window.",[59,183,184],{"class":69},"removeEventListener",[59,186,161],{"class":73},[59,188,165],{"class":164},[59,190,168],{"class":73},[59,192,194],{"class":61,"line":193},8,[59,195,196],{"class":73},"  }, [])\n",[59,198,200],{"class":61,"line":199},9,[59,201,112],{"emptyLinePlaceholder":111},[59,203,205,208],{"class":61,"line":204},10,[59,206,207],{"class":65},"  return",[59,209,210],{"class":73}," width\n",[59,212,214],{"class":61,"line":213},11,[59,215,216],{"class":73},"}\n",[59,218,220],{"class":61,"line":219},12,[59,221,112],{"emptyLinePlaceholder":111},[59,223,225],{"class":61,"line":224},13,[59,226,228],{"class":227},"sJ8bj","\u002F\u002F any component can use it\n",[59,230,232,235,238,240,242],{"class":61,"line":231},14,[59,233,234],{"class":65},"const",[59,236,237],{"class":86}," width",[59,239,139],{"class":65},[59,241,70],{"class":69},[59,243,244],{"class":73},"()\n",[15,246,247,248,251,252,255,256,258,259,262],{},"Each component that calls ",[26,249,250],{},"useWindowWidth"," gets its ",[18,253,254],{},"own isolated state",". Custom hooks\nare not singletons — calling the same hook in two components gives each its own ",[26,257,87],{},"\nand ",[26,260,261],{},"handler",".",[10,264,266,267,269],{"id":265},"why-the-use-prefix-is-required","Why the ",[26,268,47],{}," prefix is required",[15,271,272,273,275,276,279],{},"The ",[26,274,47],{}," prefix is how React and the ",[26,277,278],{},"eslint-plugin-react-hooks"," linter identify hook\nfunctions. Without it, the linter won't apply the rules of hooks (called at top level,\nnot in conditions) to your function's internals — and violations inside it go unchecked.",[50,281,283],{"className":52,"code":282,"language":54,"meta":55,"style":55},"\u002F\u002F linter treats this as a plain function — hook violations inside are invisible\nfunction getTheme() {\n  const [theme] = useState('dark') \u002F\u002F no lint warning for calling inside a condition\n  return theme\n}\n\n\u002F\u002F linter applies rules of hooks — violations are caught\nfunction useTheme() {\n  const [theme] = useState('dark')\n  return theme\n}\n",[26,284,285,290,299,325,332,336,340,345,354,375,381],{"__ignoreMap":55},[59,286,287],{"class":61,"line":62},[59,288,289],{"class":227},"\u002F\u002F linter treats this as a plain function — hook violations inside are invisible\n",[59,291,292,294,297],{"class":61,"line":77},[59,293,66],{"class":65},[59,295,296],{"class":69}," getTheme",[59,298,74],{"class":73},[59,300,301,303,305,308,310,312,314,316,319,322],{"class":61,"line":108},[59,302,80],{"class":65},[59,304,83],{"class":73},[59,306,307],{"class":86},"theme",[59,309,96],{"class":73},[59,311,99],{"class":65},[59,313,102],{"class":69},[59,315,161],{"class":73},[59,317,318],{"class":164},"'dark'",[59,320,321],{"class":73},") ",[59,323,324],{"class":227},"\u002F\u002F no lint warning for calling inside a condition\n",[59,326,327,329],{"class":61,"line":115},[59,328,207],{"class":65},[59,330,331],{"class":73}," theme\n",[59,333,334],{"class":61,"line":130},[59,335,216],{"class":73},[59,337,338],{"class":61,"line":152},[59,339,112],{"emptyLinePlaceholder":111},[59,341,342],{"class":61,"line":171},[59,343,344],{"class":227},"\u002F\u002F linter applies rules of hooks — violations are caught\n",[59,346,347,349,352],{"class":61,"line":193},[59,348,66],{"class":65},[59,350,351],{"class":69}," useTheme",[59,353,74],{"class":73},[59,355,356,358,360,362,364,366,368,370,372],{"class":61,"line":199},[59,357,80],{"class":65},[59,359,83],{"class":73},[59,361,307],{"class":86},[59,363,96],{"class":73},[59,365,99],{"class":65},[59,367,102],{"class":69},[59,369,161],{"class":73},[59,371,318],{"class":164},[59,373,374],{"class":73},")\n",[59,376,377,379],{"class":61,"line":204},[59,378,207],{"class":65},[59,380,331],{"class":73},[59,382,383],{"class":61,"line":213},[59,384,216],{"class":73},[15,386,387],{},"The prefix is also a convention that signals to other developers: \"this function uses\nhooks and must be called at the top level of a component or another hook.\"",[10,389,391],{"id":390},"custom-hook-vs-utility-function","Custom hook vs. utility function",[15,393,394,395,398,399,403],{},"If a function doesn't call any hooks, it's a ",[18,396,397],{},"utility function"," — it can be called\nanywhere, including outside React. A custom hook ",[400,401,402],"em",{},"requires"," hooks, which means it must\nfollow the rules of hooks.",[50,405,407],{"className":52,"code":406,"language":54,"meta":55,"style":55},"\u002F\u002F utility: no hooks, usable anywhere\nfunction formatCurrency(amount, locale) {\n  return new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(amount)\n}\n\n\u002F\u002F custom hook: has a hook, must be called at top level of a component\nfunction useCurrency(amount) {\n  const { locale } = useContext(LocaleContext) \u002F\u002F uses a hook\n  return formatCurrency(amount, locale)\n}\n",[26,408,409,414,435,469,473,477,482,495,518,527],{"__ignoreMap":55},[59,410,411],{"class":61,"line":62},[59,412,413],{"class":227},"\u002F\u002F utility: no hooks, usable anywhere\n",[59,415,416,418,421,423,427,429,432],{"class":61,"line":77},[59,417,66],{"class":65},[59,419,420],{"class":69}," formatCurrency",[59,422,161],{"class":73},[59,424,426],{"class":425},"s4XuR","amount",[59,428,90],{"class":73},[59,430,431],{"class":425},"locale",[59,433,434],{"class":73},") {\n",[59,436,437,439,442,445,448,451,454,457,460,463,466],{"class":61,"line":108},[59,438,207],{"class":65},[59,440,441],{"class":65}," new",[59,443,444],{"class":73}," Intl.",[59,446,447],{"class":69},"NumberFormat",[59,449,450],{"class":73},"(locale, { style: ",[59,452,453],{"class":164},"'currency'",[59,455,456],{"class":73},", currency: ",[59,458,459],{"class":164},"'USD'",[59,461,462],{"class":73}," }).",[59,464,465],{"class":69},"format",[59,467,468],{"class":73},"(amount)\n",[59,470,471],{"class":61,"line":115},[59,472,216],{"class":73},[59,474,475],{"class":61,"line":130},[59,476,112],{"emptyLinePlaceholder":111},[59,478,479],{"class":61,"line":152},[59,480,481],{"class":227},"\u002F\u002F custom hook: has a hook, must be called at top level of a component\n",[59,483,484,486,489,491,493],{"class":61,"line":171},[59,485,66],{"class":65},[59,487,488],{"class":69}," useCurrency",[59,490,161],{"class":73},[59,492,426],{"class":425},[59,494,434],{"class":73},[59,496,497,499,502,504,507,509,512,515],{"class":61,"line":193},[59,498,80],{"class":65},[59,500,501],{"class":73}," { ",[59,503,431],{"class":86},[59,505,506],{"class":73}," } ",[59,508,99],{"class":65},[59,510,511],{"class":69}," useContext",[59,513,514],{"class":73},"(LocaleContext) ",[59,516,517],{"class":227},"\u002F\u002F uses a hook\n",[59,519,520,522,524],{"class":61,"line":199},[59,521,207],{"class":65},[59,523,420],{"class":69},[59,525,526],{"class":73},"(amount, locale)\n",[59,528,529],{"class":61,"line":204},[59,530,216],{"class":73},[15,532,533],{},"Extract to a custom hook only when you genuinely need React hooks. Otherwise, a plain\nutility function is cleaner and more portable.",[10,535,537],{"id":536},"custom-hooks-vs-hocs-and-render-props","Custom hooks vs. HOCs and render props",[15,539,540],{},"All three patterns share stateful logic across components. Custom hooks win on simplicity:",[542,543,544,562],"table",{},[545,546,547],"thead",{},[548,549,550,553,556,559],"tr",{},[551,552],"th",{},[551,554,555],{},"HOC",[551,557,558],{},"Render prop",[551,560,561],{},"Custom hook",[563,564,565,579,590,604],"tbody",{},[548,566,567,571,574,576],{},[568,569,570],"td",{},"Extra component in tree",[568,572,573],{},"Yes",[568,575,573],{},[568,577,578],{},"No",[548,580,581,584,586,588],{},[568,582,583],{},"Props renaming conflicts",[568,585,573],{},[568,587,578],{},[568,589,578],{},[548,591,592,595,598,601],{},[568,593,594],{},"TypeScript inference",[568,596,597],{},"Hard",[568,599,600],{},"OK",[568,602,603],{},"Excellent",[548,605,606,609,611,613],{},[568,607,608],{},"JSX changes needed",[568,610,573],{},[568,612,573],{},[568,614,578],{},[50,616,618],{"className":52,"code":617,"language":54,"meta":55,"style":55},"\u002F\u002F HOC — wraps the component\nconst EnhancedPage = withWindowWidth(Page)\n\n\u002F\u002F render prop — restructures JSX\n\u003CWindowWidth>{width => \u003CPage width={width} \u002F>}\u003C\u002FWindowWidth>\n\n\u002F\u002F custom hook — plain function call\nfunction Page() {\n  const width = useWindowWidth()\n  return \u003Cdiv>{width}\u003C\u002Fdiv>\n}\n",[26,619,620,625,640,644,649,683,687,692,701,713,730],{"__ignoreMap":55},[59,621,622],{"class":61,"line":62},[59,623,624],{"class":227},"\u002F\u002F HOC — wraps the component\n",[59,626,627,629,632,634,637],{"class":61,"line":77},[59,628,234],{"class":65},[59,630,631],{"class":86}," EnhancedPage",[59,633,139],{"class":65},[59,635,636],{"class":69}," withWindowWidth",[59,638,639],{"class":73},"(Page)\n",[59,641,642],{"class":61,"line":108},[59,643,112],{"emptyLinePlaceholder":111},[59,645,646],{"class":61,"line":115},[59,647,648],{"class":227},"\u002F\u002F render prop — restructures JSX\n",[59,650,651,654,657,660,662,665,668,671,673,675,678,680],{"class":61,"line":130},[59,652,653],{"class":73},"\u003C",[59,655,656],{"class":86},"WindowWidth",[59,658,659],{"class":73},">{",[59,661,87],{"class":425},[59,663,664],{"class":65}," =>",[59,666,667],{"class":73}," \u003C",[59,669,670],{"class":86},"Page",[59,672,237],{"class":69},[59,674,99],{"class":65},[59,676,677],{"class":73},"{width} \u002F>}\u003C\u002F",[59,679,656],{"class":86},[59,681,682],{"class":73},">\n",[59,684,685],{"class":61,"line":152},[59,686,112],{"emptyLinePlaceholder":111},[59,688,689],{"class":61,"line":171},[59,690,691],{"class":227},"\u002F\u002F custom hook — plain function call\n",[59,693,694,696,699],{"class":61,"line":193},[59,695,66],{"class":65},[59,697,698],{"class":69}," Page",[59,700,74],{"class":73},[59,702,703,705,707,709,711],{"class":61,"line":199},[59,704,80],{"class":65},[59,706,237],{"class":86},[59,708,139],{"class":65},[59,710,70],{"class":69},[59,712,244],{"class":73},[59,714,715,717,719,723,726,728],{"class":61,"line":204},[59,716,207],{"class":65},[59,718,667],{"class":73},[59,720,722],{"class":721},"s9eBZ","div",[59,724,725],{"class":73},">{width}\u003C\u002F",[59,727,722],{"class":721},[59,729,682],{"class":73},[59,731,732],{"class":61,"line":213},[59,733,216],{"class":73},[15,735,736],{},"Custom hooks are the primary reason HOCs and render props have largely faded from modern\nReact codebases.",[10,738,740],{"id":739},"essential-patterns-every-interviewer-expects","Essential patterns every interviewer expects",[742,743,745],"h3",{"id":744},"useprevious","usePrevious",[15,747,748],{},"Track the value from the previous render using a ref that updates after each render.",[50,750,752],{"className":52,"code":751,"language":54,"meta":55,"style":55},"function usePrevious(value) {\n  const ref = useRef()\n  useEffect(() => { ref.current = value }) \u002F\u002F runs after render\n  return ref.current                        \u002F\u002F returns last render's value\n}\n\nconst prevCount = usePrevious(count)\n\u002F\u002F during render: ref.current is still the value from the previous render\n\u002F\u002F after render: useEffect fires and syncs it to the current value\n",[26,753,754,768,782,801,811,815,819,833,838],{"__ignoreMap":55},[59,755,756,758,761,763,766],{"class":61,"line":62},[59,757,66],{"class":65},[59,759,760],{"class":69}," usePrevious",[59,762,161],{"class":73},[59,764,765],{"class":425},"value",[59,767,434],{"class":73},[59,769,770,772,775,777,780],{"class":61,"line":77},[59,771,80],{"class":65},[59,773,774],{"class":86}," ref",[59,776,139],{"class":65},[59,778,779],{"class":69}," useRef",[59,781,244],{"class":73},[59,783,784,786,788,790,793,795,798],{"class":61,"line":108},[59,785,118],{"class":69},[59,787,121],{"class":73},[59,789,124],{"class":65},[59,791,792],{"class":73}," { ref.current ",[59,794,99],{"class":65},[59,796,797],{"class":73}," value }) ",[59,799,800],{"class":227},"\u002F\u002F runs after render\n",[59,802,803,805,808],{"class":61,"line":115},[59,804,207],{"class":65},[59,806,807],{"class":73}," ref.current                        ",[59,809,810],{"class":227},"\u002F\u002F returns last render's value\n",[59,812,813],{"class":61,"line":130},[59,814,216],{"class":73},[59,816,817],{"class":61,"line":152},[59,818,112],{"emptyLinePlaceholder":111},[59,820,821,823,826,828,830],{"class":61,"line":171},[59,822,234],{"class":65},[59,824,825],{"class":86}," prevCount",[59,827,139],{"class":65},[59,829,760],{"class":69},[59,831,832],{"class":73},"(count)\n",[59,834,835],{"class":61,"line":193},[59,836,837],{"class":227},"\u002F\u002F during render: ref.current is still the value from the previous render\n",[59,839,840],{"class":61,"line":199},[59,841,842],{"class":227},"\u002F\u002F after render: useEffect fires and syncs it to the current value\n",[742,844,28],{"id":845},"usedebounce",[15,847,848],{},"Delay updating a \"debounced\" value until input settles for a given time.",[50,850,852],{"className":52,"code":851,"language":54,"meta":55,"style":55},"function useDebounce(value, delay = 300) {\n  const [debounced, setDebounced] = useState(value)\n\n  useEffect(() => {\n    const id = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(id) \u002F\u002F cancel on next change\n  }, [value, delay])\n\n  return debounced\n}\n\n\u002F\u002F usage\nconst debouncedQuery = useDebounce(query, 400)\n\u002F\u002F run search on debouncedQuery, not query\n",[26,853,854,877,900,904,914,936,953,958,962,969,973,977,982,1001],{"__ignoreMap":55},[59,855,856,858,861,863,865,867,870,872,875],{"class":61,"line":62},[59,857,66],{"class":65},[59,859,860],{"class":69}," useDebounce",[59,862,161],{"class":73},[59,864,765],{"class":425},[59,866,90],{"class":73},[59,868,869],{"class":425},"delay",[59,871,139],{"class":65},[59,873,874],{"class":86}," 300",[59,876,434],{"class":73},[59,878,879,881,883,886,888,891,893,895,897],{"class":61,"line":77},[59,880,80],{"class":65},[59,882,83],{"class":73},[59,884,885],{"class":86},"debounced",[59,887,90],{"class":73},[59,889,890],{"class":86},"setDebounced",[59,892,96],{"class":73},[59,894,99],{"class":65},[59,896,102],{"class":69},[59,898,899],{"class":73},"(value)\n",[59,901,902],{"class":61,"line":108},[59,903,112],{"emptyLinePlaceholder":111},[59,905,906,908,910,912],{"class":61,"line":115},[59,907,118],{"class":69},[59,909,121],{"class":73},[59,911,124],{"class":65},[59,913,127],{"class":73},[59,915,916,918,921,923,926,928,930,933],{"class":61,"line":130},[59,917,133],{"class":65},[59,919,920],{"class":86}," id",[59,922,139],{"class":65},[59,924,925],{"class":69}," setTimeout",[59,927,121],{"class":73},[59,929,124],{"class":65},[59,931,932],{"class":69}," setDebounced",[59,934,935],{"class":73},"(value), delay)\n",[59,937,938,940,942,944,947,950],{"class":61,"line":152},[59,939,174],{"class":65},[59,941,142],{"class":73},[59,943,124],{"class":65},[59,945,946],{"class":69}," clearTimeout",[59,948,949],{"class":73},"(id) ",[59,951,952],{"class":227},"\u002F\u002F cancel on next change\n",[59,954,955],{"class":61,"line":171},[59,956,957],{"class":73},"  }, [value, delay])\n",[59,959,960],{"class":61,"line":193},[59,961,112],{"emptyLinePlaceholder":111},[59,963,964,966],{"class":61,"line":199},[59,965,207],{"class":65},[59,967,968],{"class":73}," debounced\n",[59,970,971],{"class":61,"line":204},[59,972,216],{"class":73},[59,974,975],{"class":61,"line":213},[59,976,112],{"emptyLinePlaceholder":111},[59,978,979],{"class":61,"line":219},[59,980,981],{"class":227},"\u002F\u002F usage\n",[59,983,984,986,989,991,993,996,999],{"class":61,"line":224},[59,985,234],{"class":65},[59,987,988],{"class":86}," debouncedQuery",[59,990,139],{"class":65},[59,992,860],{"class":69},[59,994,995],{"class":73},"(query, ",[59,997,998],{"class":86},"400",[59,1000,374],{"class":73},[59,1002,1003],{"class":61,"line":231},[59,1004,1005],{"class":227},"\u002F\u002F run search on debouncedQuery, not query\n",[15,1007,1008,1009,1011,1012,1014],{},"The cleanup cancels the pending timeout on each change, so ",[26,1010,890],{}," only fires\nafter the user pauses for ",[26,1013,869],{}," milliseconds.",[742,1016,1018],{"id":1017},"uselocalstorage","useLocalStorage",[15,1020,1021,1022,1025,1026,262],{},"Persist state to ",[26,1023,1024],{},"localStorage"," with the same interface as ",[26,1027,1028],{},"useState",[50,1030,1032],{"className":52,"code":1031,"language":54,"meta":55,"style":55},"function useLocalStorage(key, initial) {\n  const [value, setValue] = useState(() => {\n    try {\n      const stored = localStorage.getItem(key)\n      return stored !== null ? JSON.parse(stored) : initial\n    } catch {\n      return initial\n    }\n  })\n\n  useEffect(() => {\n    localStorage.setItem(key, JSON.stringify(value))\n  }, [key, value])\n\n  return [value, setValue]\n}\n\nconst [theme, setTheme] = useLocalStorage('theme', 'dark')\n",[26,1033,1034,1053,1078,1085,1104,1138,1148,1154,1159,1164,1168,1178,1200,1205,1209,1217,1222,1227],{"__ignoreMap":55},[59,1035,1036,1038,1041,1043,1046,1048,1051],{"class":61,"line":62},[59,1037,66],{"class":65},[59,1039,1040],{"class":69}," useLocalStorage",[59,1042,161],{"class":73},[59,1044,1045],{"class":425},"key",[59,1047,90],{"class":73},[59,1049,1050],{"class":425},"initial",[59,1052,434],{"class":73},[59,1054,1055,1057,1059,1061,1063,1066,1068,1070,1072,1074,1076],{"class":61,"line":77},[59,1056,80],{"class":65},[59,1058,83],{"class":73},[59,1060,765],{"class":86},[59,1062,90],{"class":73},[59,1064,1065],{"class":86},"setValue",[59,1067,96],{"class":73},[59,1069,99],{"class":65},[59,1071,102],{"class":69},[59,1073,121],{"class":73},[59,1075,124],{"class":65},[59,1077,127],{"class":73},[59,1079,1080,1083],{"class":61,"line":108},[59,1081,1082],{"class":65},"    try",[59,1084,127],{"class":73},[59,1086,1087,1090,1093,1095,1098,1101],{"class":61,"line":115},[59,1088,1089],{"class":65},"      const",[59,1091,1092],{"class":86}," stored",[59,1094,139],{"class":65},[59,1096,1097],{"class":73}," localStorage.",[59,1099,1100],{"class":69},"getItem",[59,1102,1103],{"class":73},"(key)\n",[59,1105,1106,1109,1112,1115,1118,1121,1124,1126,1129,1132,1135],{"class":61,"line":130},[59,1107,1108],{"class":65},"      return",[59,1110,1111],{"class":73}," stored ",[59,1113,1114],{"class":65},"!==",[59,1116,1117],{"class":86}," null",[59,1119,1120],{"class":65}," ?",[59,1122,1123],{"class":86}," JSON",[59,1125,262],{"class":73},[59,1127,1128],{"class":69},"parse",[59,1130,1131],{"class":73},"(stored) ",[59,1133,1134],{"class":65},":",[59,1136,1137],{"class":73}," initial\n",[59,1139,1140,1143,1146],{"class":61,"line":152},[59,1141,1142],{"class":73},"    } ",[59,1144,1145],{"class":65},"catch",[59,1147,127],{"class":73},[59,1149,1150,1152],{"class":61,"line":171},[59,1151,1108],{"class":65},[59,1153,1137],{"class":73},[59,1155,1156],{"class":61,"line":193},[59,1157,1158],{"class":73},"    }\n",[59,1160,1161],{"class":61,"line":199},[59,1162,1163],{"class":73},"  })\n",[59,1165,1166],{"class":61,"line":204},[59,1167,112],{"emptyLinePlaceholder":111},[59,1169,1170,1172,1174,1176],{"class":61,"line":213},[59,1171,118],{"class":69},[59,1173,121],{"class":73},[59,1175,124],{"class":65},[59,1177,127],{"class":73},[59,1179,1180,1183,1186,1189,1192,1194,1197],{"class":61,"line":219},[59,1181,1182],{"class":73},"    localStorage.",[59,1184,1185],{"class":69},"setItem",[59,1187,1188],{"class":73},"(key, ",[59,1190,1191],{"class":86},"JSON",[59,1193,262],{"class":73},[59,1195,1196],{"class":69},"stringify",[59,1198,1199],{"class":73},"(value))\n",[59,1201,1202],{"class":61,"line":224},[59,1203,1204],{"class":73},"  }, [key, value])\n",[59,1206,1207],{"class":61,"line":231},[59,1208,112],{"emptyLinePlaceholder":111},[59,1210,1212,1214],{"class":61,"line":1211},15,[59,1213,207],{"class":65},[59,1215,1216],{"class":73}," [value, setValue]\n",[59,1218,1220],{"class":61,"line":1219},16,[59,1221,216],{"class":73},[59,1223,1225],{"class":61,"line":1224},17,[59,1226,112],{"emptyLinePlaceholder":111},[59,1228,1230,1232,1234,1236,1238,1241,1243,1245,1247,1249,1252,1254,1256],{"class":61,"line":1229},18,[59,1231,234],{"class":65},[59,1233,83],{"class":73},[59,1235,307],{"class":86},[59,1237,90],{"class":73},[59,1239,1240],{"class":86},"setTheme",[59,1242,96],{"class":73},[59,1244,99],{"class":65},[59,1246,1040],{"class":69},[59,1248,161],{"class":73},[59,1250,1251],{"class":164},"'theme'",[59,1253,90],{"class":73},[59,1255,318],{"class":164},[59,1257,374],{"class":73},[15,1259,1260,1261,1264,1265,1267],{},"The lazy initializer reads storage once on mount; the effect persists every change.\nThe ",[26,1262,1263],{},"try\u002Fcatch"," handles SSR environments where ",[26,1266,1024],{}," doesn't exist.",[742,1269,1271],{"id":1270},"usemediaquery","useMediaQuery",[15,1273,1274],{},"Subscribe to a CSS media query and return whether it currently matches.",[50,1276,1278],{"className":52,"code":1277,"language":54,"meta":55,"style":55},"function useMediaQuery(query) {\n  const [matches, setMatches] = useState(\n    () => typeof window !== 'undefined' && window.matchMedia(query).matches\n  )\n\n  useEffect(() => {\n    const mq = window.matchMedia(query)\n    const handler = e => setMatches(e.matches)\n    mq.addEventListener('change', handler)\n    return () => mq.removeEventListener('change', handler)\n  }, [query])\n\n  return matches\n}\n\nconst isDesktop = useMediaQuery('(min-width: 1024px)')\n",[26,1279,1280,1294,1317,1346,1351,1355,1365,1381,1400,1414,1433,1438,1442,1449,1453,1457],{"__ignoreMap":55},[59,1281,1282,1284,1287,1289,1292],{"class":61,"line":62},[59,1283,66],{"class":65},[59,1285,1286],{"class":69}," useMediaQuery",[59,1288,161],{"class":73},[59,1290,1291],{"class":425},"query",[59,1293,434],{"class":73},[59,1295,1296,1298,1300,1303,1305,1308,1310,1312,1314],{"class":61,"line":77},[59,1297,80],{"class":65},[59,1299,83],{"class":73},[59,1301,1302],{"class":86},"matches",[59,1304,90],{"class":73},[59,1306,1307],{"class":86},"setMatches",[59,1309,96],{"class":73},[59,1311,99],{"class":65},[59,1313,102],{"class":69},[59,1315,1316],{"class":73},"(\n",[59,1318,1319,1322,1324,1327,1330,1332,1335,1338,1340,1343],{"class":61,"line":108},[59,1320,1321],{"class":73},"    () ",[59,1323,124],{"class":65},[59,1325,1326],{"class":65}," typeof",[59,1328,1329],{"class":73}," window ",[59,1331,1114],{"class":65},[59,1333,1334],{"class":164}," 'undefined'",[59,1336,1337],{"class":65}," &&",[59,1339,181],{"class":73},[59,1341,1342],{"class":69},"matchMedia",[59,1344,1345],{"class":73},"(query).matches\n",[59,1347,1348],{"class":61,"line":115},[59,1349,1350],{"class":73},"  )\n",[59,1352,1353],{"class":61,"line":130},[59,1354,112],{"emptyLinePlaceholder":111},[59,1356,1357,1359,1361,1363],{"class":61,"line":152},[59,1358,118],{"class":69},[59,1360,121],{"class":73},[59,1362,124],{"class":65},[59,1364,127],{"class":73},[59,1366,1367,1369,1372,1374,1376,1378],{"class":61,"line":171},[59,1368,133],{"class":65},[59,1370,1371],{"class":86}," mq",[59,1373,139],{"class":65},[59,1375,181],{"class":73},[59,1377,1342],{"class":69},[59,1379,1380],{"class":73},"(query)\n",[59,1382,1383,1385,1387,1389,1392,1394,1397],{"class":61,"line":193},[59,1384,133],{"class":65},[59,1386,136],{"class":69},[59,1388,139],{"class":65},[59,1390,1391],{"class":425}," e",[59,1393,664],{"class":65},[59,1395,1396],{"class":69}," setMatches",[59,1398,1399],{"class":73},"(e.matches)\n",[59,1401,1402,1405,1407,1409,1412],{"class":61,"line":199},[59,1403,1404],{"class":73},"    mq.",[59,1406,158],{"class":69},[59,1408,161],{"class":73},[59,1410,1411],{"class":164},"'change'",[59,1413,168],{"class":73},[59,1415,1416,1418,1420,1422,1425,1427,1429,1431],{"class":61,"line":204},[59,1417,174],{"class":65},[59,1419,142],{"class":73},[59,1421,124],{"class":65},[59,1423,1424],{"class":73}," mq.",[59,1426,184],{"class":69},[59,1428,161],{"class":73},[59,1430,1411],{"class":164},[59,1432,168],{"class":73},[59,1434,1435],{"class":61,"line":213},[59,1436,1437],{"class":73},"  }, [query])\n",[59,1439,1440],{"class":61,"line":219},[59,1441,112],{"emptyLinePlaceholder":111},[59,1443,1444,1446],{"class":61,"line":224},[59,1445,207],{"class":65},[59,1447,1448],{"class":73}," matches\n",[59,1450,1451],{"class":61,"line":231},[59,1452,216],{"class":73},[59,1454,1455],{"class":61,"line":1211},[59,1456,112],{"emptyLinePlaceholder":111},[59,1458,1459,1461,1464,1466,1468,1470,1473],{"class":61,"line":1219},[59,1460,234],{"class":65},[59,1462,1463],{"class":86}," isDesktop",[59,1465,139],{"class":65},[59,1467,1286],{"class":69},[59,1469,161],{"class":73},[59,1471,1472],{"class":164},"'(min-width: 1024px)'",[59,1474,374],{"class":73},[15,1476,272,1477,1480],{},[26,1478,1479],{},"typeof window !== 'undefined'"," guard prevents SSR crashes.",[742,1482,1484],{"id":1483},"usedatafetch","useDataFetch",[15,1486,1487],{},"Encapsulate the loading\u002Ferror\u002Fdata state pattern that appears in every component that\nfetches data.",[50,1489,1491],{"className":52,"code":1490,"language":54,"meta":55,"style":55},"function useDataFetch(url) {\n  const [state, setState] = useState({ data: null, loading: true, error: null })\n\n  useEffect(() => {\n    let active = true\n    setState({ data: null, loading: true, error: null })\n    fetch(url)\n      .then(r => r.json())\n      .then(data  => { if (active) setState({ data, loading: false, error: null }) })\n      .catch(error => { if (active) setState({ data: null, loading: false, error }) })\n    return () => { active = false }\n  }, [url])\n\n  return state\n}\n\nconst { data, loading, error } = useDataFetch(`\u002Fapi\u002Fuser\u002F${id}`)\n",[26,1492,1493,1507,1547,1551,1561,1574,1593,1601,1625,1662,1694,1713,1718,1722,1729,1733,1737],{"__ignoreMap":55},[59,1494,1495,1497,1500,1502,1505],{"class":61,"line":62},[59,1496,66],{"class":65},[59,1498,1499],{"class":69}," useDataFetch",[59,1501,161],{"class":73},[59,1503,1504],{"class":425},"url",[59,1506,434],{"class":73},[59,1508,1509,1511,1513,1516,1518,1521,1523,1525,1527,1530,1533,1536,1539,1542,1544],{"class":61,"line":77},[59,1510,80],{"class":65},[59,1512,83],{"class":73},[59,1514,1515],{"class":86},"state",[59,1517,90],{"class":73},[59,1519,1520],{"class":86},"setState",[59,1522,96],{"class":73},[59,1524,99],{"class":65},[59,1526,102],{"class":69},[59,1528,1529],{"class":73},"({ data: ",[59,1531,1532],{"class":86},"null",[59,1534,1535],{"class":73},", loading: ",[59,1537,1538],{"class":86},"true",[59,1540,1541],{"class":73},", error: ",[59,1543,1532],{"class":86},[59,1545,1546],{"class":73}," })\n",[59,1548,1549],{"class":61,"line":108},[59,1550,112],{"emptyLinePlaceholder":111},[59,1552,1553,1555,1557,1559],{"class":61,"line":115},[59,1554,118],{"class":69},[59,1556,121],{"class":73},[59,1558,124],{"class":65},[59,1560,127],{"class":73},[59,1562,1563,1566,1569,1571],{"class":61,"line":130},[59,1564,1565],{"class":65},"    let",[59,1567,1568],{"class":73}," active ",[59,1570,99],{"class":65},[59,1572,1573],{"class":86}," true\n",[59,1575,1576,1579,1581,1583,1585,1587,1589,1591],{"class":61,"line":152},[59,1577,1578],{"class":69},"    setState",[59,1580,1529],{"class":73},[59,1582,1532],{"class":86},[59,1584,1535],{"class":73},[59,1586,1538],{"class":86},[59,1588,1541],{"class":73},[59,1590,1532],{"class":86},[59,1592,1546],{"class":73},[59,1594,1595,1598],{"class":61,"line":171},[59,1596,1597],{"class":69},"    fetch",[59,1599,1600],{"class":73},"(url)\n",[59,1602,1603,1606,1609,1611,1614,1616,1619,1622],{"class":61,"line":193},[59,1604,1605],{"class":73},"      .",[59,1607,1608],{"class":69},"then",[59,1610,161],{"class":73},[59,1612,1613],{"class":425},"r",[59,1615,664],{"class":65},[59,1617,1618],{"class":73}," r.",[59,1620,1621],{"class":69},"json",[59,1623,1624],{"class":73},"())\n",[59,1626,1627,1629,1631,1633,1636,1639,1641,1644,1647,1649,1652,1655,1657,1659],{"class":61,"line":199},[59,1628,1605],{"class":73},[59,1630,1608],{"class":69},[59,1632,161],{"class":73},[59,1634,1635],{"class":425},"data",[59,1637,1638],{"class":65},"  =>",[59,1640,501],{"class":73},[59,1642,1643],{"class":65},"if",[59,1645,1646],{"class":73}," (active) ",[59,1648,1520],{"class":69},[59,1650,1651],{"class":73},"({ data, loading: ",[59,1653,1654],{"class":86},"false",[59,1656,1541],{"class":73},[59,1658,1532],{"class":86},[59,1660,1661],{"class":73}," }) })\n",[59,1663,1664,1666,1668,1670,1673,1675,1677,1679,1681,1683,1685,1687,1689,1691],{"class":61,"line":204},[59,1665,1605],{"class":73},[59,1667,1145],{"class":69},[59,1669,161],{"class":73},[59,1671,1672],{"class":425},"error",[59,1674,664],{"class":65},[59,1676,501],{"class":73},[59,1678,1643],{"class":65},[59,1680,1646],{"class":73},[59,1682,1520],{"class":69},[59,1684,1529],{"class":73},[59,1686,1532],{"class":86},[59,1688,1535],{"class":73},[59,1690,1654],{"class":86},[59,1692,1693],{"class":73},", error }) })\n",[59,1695,1696,1698,1700,1702,1705,1707,1710],{"class":61,"line":213},[59,1697,174],{"class":65},[59,1699,142],{"class":73},[59,1701,124],{"class":65},[59,1703,1704],{"class":73}," { active ",[59,1706,99],{"class":65},[59,1708,1709],{"class":86}," false",[59,1711,1712],{"class":73}," }\n",[59,1714,1715],{"class":61,"line":219},[59,1716,1717],{"class":73},"  }, [url])\n",[59,1719,1720],{"class":61,"line":224},[59,1721,112],{"emptyLinePlaceholder":111},[59,1723,1724,1726],{"class":61,"line":231},[59,1725,207],{"class":65},[59,1727,1728],{"class":73}," state\n",[59,1730,1731],{"class":61,"line":1211},[59,1732,216],{"class":73},[59,1734,1735],{"class":61,"line":1219},[59,1736,112],{"emptyLinePlaceholder":111},[59,1738,1739,1741,1743,1745,1747,1750,1752,1754,1756,1758,1760,1762,1765,1768,1771],{"class":61,"line":1224},[59,1740,234],{"class":65},[59,1742,501],{"class":73},[59,1744,1635],{"class":86},[59,1746,90],{"class":73},[59,1748,1749],{"class":86},"loading",[59,1751,90],{"class":73},[59,1753,1672],{"class":86},[59,1755,506],{"class":73},[59,1757,99],{"class":65},[59,1759,1499],{"class":69},[59,1761,161],{"class":73},[59,1763,1764],{"class":164},"`\u002Fapi\u002Fuser\u002F${",[59,1766,1767],{"class":73},"id",[59,1769,1770],{"class":164},"}`",[59,1772,374],{"class":73},[15,1774,1775],{},"In production, prefer React Query or SWR — but writing this from scratch in an interview\nshows you understand the race-condition problem and its solution.",[10,1777,1779],{"id":1778},"hook-composition","Hook composition",[15,1781,1782],{},"Custom hooks can call other custom hooks, enabling a layered architecture that mirrors\nhow components compose.",[50,1784,1786],{"className":52,"code":1785,"language":54,"meta":55,"style":55},"function useUserPreferences() {\n  const [theme, setTheme] = useLocalStorage('theme', 'dark') \u002F\u002F custom hook\n  const isDesktop = useMediaQuery('(min-width: 1024px)')      \u002F\u002F custom hook\n  const { locale } = useContext(LocaleContext)\n  return { theme, setTheme, isDesktop, locale }\n}\n",[26,1787,1788,1797,1828,1847,1864,1871],{"__ignoreMap":55},[59,1789,1790,1792,1795],{"class":61,"line":62},[59,1791,66],{"class":65},[59,1793,1794],{"class":69}," useUserPreferences",[59,1796,74],{"class":73},[59,1798,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823,1825],{"class":61,"line":77},[59,1800,80],{"class":65},[59,1802,83],{"class":73},[59,1804,307],{"class":86},[59,1806,90],{"class":73},[59,1808,1240],{"class":86},[59,1810,96],{"class":73},[59,1812,99],{"class":65},[59,1814,1040],{"class":69},[59,1816,161],{"class":73},[59,1818,1251],{"class":164},[59,1820,90],{"class":73},[59,1822,318],{"class":164},[59,1824,321],{"class":73},[59,1826,1827],{"class":227},"\u002F\u002F custom hook\n",[59,1829,1830,1832,1834,1836,1838,1840,1842,1845],{"class":61,"line":108},[59,1831,80],{"class":65},[59,1833,1463],{"class":86},[59,1835,139],{"class":65},[59,1837,1286],{"class":69},[59,1839,161],{"class":73},[59,1841,1472],{"class":164},[59,1843,1844],{"class":73},")      ",[59,1846,1827],{"class":227},[59,1848,1849,1851,1853,1855,1857,1859,1861],{"class":61,"line":115},[59,1850,80],{"class":65},[59,1852,501],{"class":73},[59,1854,431],{"class":86},[59,1856,506],{"class":73},[59,1858,99],{"class":65},[59,1860,511],{"class":69},[59,1862,1863],{"class":73},"(LocaleContext)\n",[59,1865,1866,1868],{"class":61,"line":130},[59,1867,207],{"class":65},[59,1869,1870],{"class":73}," { theme, setTheme, isDesktop, locale }\n",[59,1872,1873],{"class":61,"line":152},[59,1874,216],{"class":73},[15,1876,1877],{},"Each layer stays focused and individually testable. The composed hook just orchestrates\nthem rather than re-implementing each concern.",[10,1879,1881],{"id":1880},"when-to-extract-a-custom-hook","When to extract a custom hook",[15,1883,1884],{},"Three good signals:",[1886,1887,1888,1899,1905],"ol",{},[1889,1890,1891,1894,1895,1898],"li",{},[18,1892,1893],{},"The same hook combination appears in two or more components."," Three setters for\nloading\u002Ferror\u002Fdata is a ",[26,1896,1897],{},"useFetch"," hook waiting to be written.",[1889,1900,1901,1904],{},[18,1902,1903],{},"A component's hook logic obscures what it renders."," If you have to scroll past\n20 lines of hooks to find the JSX, the hooks belong somewhere else.",[1889,1906,1907,1910,1911,1914],{},[18,1908,1909],{},"You want to test the logic independently."," A custom hook can be tested with\n",[26,1912,1913],{},"renderHook"," without rendering the full component.",[15,1916,1917],{},"Don't extract proactively. A hook with a single caller adds indirection without benefit.",[10,1919,1921],{"id":1920},"what-can-a-custom-hook-return","What can a custom hook return?",[15,1923,1924],{},"Anything — a single value, a tuple, an object, or nothing.",[1926,1927,1928,1940,1949],"ul",{},[1889,1929,1930,1933,1934,1937,1938,262],{},[18,1931,1932],{},"Tuple"," (",[26,1935,1936],{},"[value, setter]",") — when callers will rename the parts, like ",[26,1939,1028],{},[1889,1941,1942,1933,1945,1948],{},[18,1943,1944],{},"Object",[26,1946,1947],{},"{ data, loading, error }",") — when there are several named exports and\ndestructuring by name is clearer.",[1889,1950,1951,1954],{},[18,1952,1953],{},"Single value"," — when there's only one thing to return.",[50,1956,1958],{"className":52,"code":1957,"language":54,"meta":55,"style":55},"\u002F\u002F tuple: rename freely at the call site\nconst [open, setOpen] = useToggle(false)\n\n\u002F\u002F object: pick what you need\nconst { user, isLoading } = useCurrentUser()\n",[26,1959,1960,1965,1992,1996,2001],{"__ignoreMap":55},[59,1961,1962],{"class":61,"line":62},[59,1963,1964],{"class":227},"\u002F\u002F tuple: rename freely at the call site\n",[59,1966,1967,1969,1971,1974,1976,1979,1981,1983,1986,1988,1990],{"class":61,"line":77},[59,1968,234],{"class":65},[59,1970,83],{"class":73},[59,1972,1973],{"class":86},"open",[59,1975,90],{"class":73},[59,1977,1978],{"class":86},"setOpen",[59,1980,96],{"class":73},[59,1982,99],{"class":65},[59,1984,1985],{"class":69}," useToggle",[59,1987,161],{"class":73},[59,1989,1654],{"class":86},[59,1991,374],{"class":73},[59,1993,1994],{"class":61,"line":108},[59,1995,112],{"emptyLinePlaceholder":111},[59,1997,1998],{"class":61,"line":115},[59,1999,2000],{"class":227},"\u002F\u002F object: pick what you need\n",[59,2002,2003,2005,2007,2010,2012,2015,2017,2019,2022],{"class":61,"line":130},[59,2004,234],{"class":65},[59,2006,501],{"class":73},[59,2008,2009],{"class":86},"user",[59,2011,90],{"class":73},[59,2013,2014],{"class":86},"isLoading",[59,2016,506],{"class":73},[59,2018,99],{"class":65},[59,2020,2021],{"class":69}," useCurrentUser",[59,2023,244],{"class":73},[15,2025,2026],{},"Choose based on readability at the call site, not based on convention.",[10,2028,2030],{"id":2029},"testing-custom-hooks","Testing custom hooks",[15,2032,2033,2034,2037,2038,2040,2041,2044],{},"Use ",[26,2035,2036],{},"@testing-library\u002Freact","'s ",[26,2039,1913],{}," to mount a minimal host component and assert\non the hook's return value. Wrap state updates in ",[26,2042,2043],{},"act"," so React flushes them before\nassertions.",[50,2046,2048],{"className":52,"code":2047,"language":54,"meta":55,"style":55},"import { renderHook, act } from '@testing-library\u002Freact'\nimport { useCounter } from '.\u002FuseCounter'\n\ntest('increments count', () => {\n  const { result } = renderHook(() => useCounter(0))\n  expect(result.current.count).toBe(0)\n  act(() => result.current.increment())\n  expect(result.current.count).toBe(1)\n})\n\ntest('resets to initial', () => {\n  const { result } = renderHook(() => useCounter(5))\n  act(() => result.current.reset())\n  expect(result.current.count).toBe(5)\n})\n",[26,2049,2050,2064,2076,2080,2097,2128,2145,2162,2177,2182,2186,2201,2228,2243,2257],{"__ignoreMap":55},[59,2051,2052,2055,2058,2061],{"class":61,"line":62},[59,2053,2054],{"class":65},"import",[59,2056,2057],{"class":73}," { renderHook, act } ",[59,2059,2060],{"class":65},"from",[59,2062,2063],{"class":164}," '@testing-library\u002Freact'\n",[59,2065,2066,2068,2071,2073],{"class":61,"line":77},[59,2067,2054],{"class":65},[59,2069,2070],{"class":73}," { useCounter } ",[59,2072,2060],{"class":65},[59,2074,2075],{"class":164}," '.\u002FuseCounter'\n",[59,2077,2078],{"class":61,"line":108},[59,2079,112],{"emptyLinePlaceholder":111},[59,2081,2082,2085,2087,2090,2093,2095],{"class":61,"line":115},[59,2083,2084],{"class":69},"test",[59,2086,161],{"class":73},[59,2088,2089],{"class":164},"'increments count'",[59,2091,2092],{"class":73},", () ",[59,2094,124],{"class":65},[59,2096,127],{"class":73},[59,2098,2099,2101,2103,2106,2108,2110,2113,2115,2117,2120,2122,2125],{"class":61,"line":130},[59,2100,80],{"class":65},[59,2102,501],{"class":73},[59,2104,2105],{"class":86},"result",[59,2107,506],{"class":73},[59,2109,99],{"class":65},[59,2111,2112],{"class":69}," renderHook",[59,2114,121],{"class":73},[59,2116,124],{"class":65},[59,2118,2119],{"class":69}," useCounter",[59,2121,161],{"class":73},[59,2123,2124],{"class":86},"0",[59,2126,2127],{"class":73},"))\n",[59,2129,2130,2133,2136,2139,2141,2143],{"class":61,"line":152},[59,2131,2132],{"class":69},"  expect",[59,2134,2135],{"class":73},"(result.current.count).",[59,2137,2138],{"class":69},"toBe",[59,2140,161],{"class":73},[59,2142,2124],{"class":86},[59,2144,374],{"class":73},[59,2146,2147,2150,2152,2154,2157,2160],{"class":61,"line":171},[59,2148,2149],{"class":69},"  act",[59,2151,121],{"class":73},[59,2153,124],{"class":65},[59,2155,2156],{"class":73}," result.current.",[59,2158,2159],{"class":69},"increment",[59,2161,1624],{"class":73},[59,2163,2164,2166,2168,2170,2172,2175],{"class":61,"line":193},[59,2165,2132],{"class":69},[59,2167,2135],{"class":73},[59,2169,2138],{"class":69},[59,2171,161],{"class":73},[59,2173,2174],{"class":86},"1",[59,2176,374],{"class":73},[59,2178,2179],{"class":61,"line":199},[59,2180,2181],{"class":73},"})\n",[59,2183,2184],{"class":61,"line":204},[59,2185,112],{"emptyLinePlaceholder":111},[59,2187,2188,2190,2192,2195,2197,2199],{"class":61,"line":213},[59,2189,2084],{"class":69},[59,2191,161],{"class":73},[59,2193,2194],{"class":164},"'resets to initial'",[59,2196,2092],{"class":73},[59,2198,124],{"class":65},[59,2200,127],{"class":73},[59,2202,2203,2205,2207,2209,2211,2213,2215,2217,2219,2221,2223,2226],{"class":61,"line":219},[59,2204,80],{"class":65},[59,2206,501],{"class":73},[59,2208,2105],{"class":86},[59,2210,506],{"class":73},[59,2212,99],{"class":65},[59,2214,2112],{"class":69},[59,2216,121],{"class":73},[59,2218,124],{"class":65},[59,2220,2119],{"class":69},[59,2222,161],{"class":73},[59,2224,2225],{"class":86},"5",[59,2227,2127],{"class":73},[59,2229,2230,2232,2234,2236,2238,2241],{"class":61,"line":224},[59,2231,2149],{"class":69},[59,2233,121],{"class":73},[59,2235,124],{"class":65},[59,2237,2156],{"class":73},[59,2239,2240],{"class":69},"reset",[59,2242,1624],{"class":73},[59,2244,2245,2247,2249,2251,2253,2255],{"class":61,"line":231},[59,2246,2132],{"class":69},[59,2248,2135],{"class":73},[59,2250,2138],{"class":69},[59,2252,161],{"class":73},[59,2254,2225],{"class":86},[59,2256,374],{"class":73},[59,2258,2259],{"class":61,"line":1211},[59,2260,2181],{"class":73},[15,2262,2263,2264,2266],{},"For hooks with context dependencies, wrap the ",[26,2265,1913],{}," call in the Provider:",[50,2268,2270],{"className":52,"code":2269,"language":54,"meta":55,"style":55},"renderHook(() => useAuth(), { wrapper: AuthProvider })\n",[26,2271,2272],{"__ignoreMap":55},[59,2273,2274,2276,2278,2280,2283],{"class":61,"line":62},[59,2275,1913],{"class":69},[59,2277,121],{"class":73},[59,2279,124],{"class":65},[59,2281,2282],{"class":69}," useAuth",[59,2284,2285],{"class":73},"(), { wrapper: AuthProvider })\n",[10,2287,2289],{"id":2288},"stale-closure-bugs-in-custom-hooks","Stale closure bugs in custom hooks",[15,2291,2292],{},"A custom hook can suffer stale closure bugs just like inline hook calls. If a callback\ninside the hook reads a prop or state but isn't in the dependency array, it captures a\nfrozen snapshot.",[50,2294,2296],{"className":52,"code":2295,"language":54,"meta":55,"style":55},"\u002F\u002F broken: callback captured once, never updates\nfunction useInterval(callback, delay) {\n  useEffect(() => {\n    const id = setInterval(callback, delay) \u002F\u002F stale: callback changes, interval doesn't\n    return () => clearInterval(id)\n  }, [delay])\n}\n",[26,2297,2298,2303,2321,2331,2348,2362,2367],{"__ignoreMap":55},[59,2299,2300],{"class":61,"line":62},[59,2301,2302],{"class":227},"\u002F\u002F broken: callback captured once, never updates\n",[59,2304,2305,2307,2310,2312,2315,2317,2319],{"class":61,"line":77},[59,2306,66],{"class":65},[59,2308,2309],{"class":69}," useInterval",[59,2311,161],{"class":73},[59,2313,2314],{"class":425},"callback",[59,2316,90],{"class":73},[59,2318,869],{"class":425},[59,2320,434],{"class":73},[59,2322,2323,2325,2327,2329],{"class":61,"line":108},[59,2324,118],{"class":69},[59,2326,121],{"class":73},[59,2328,124],{"class":65},[59,2330,127],{"class":73},[59,2332,2333,2335,2337,2339,2342,2345],{"class":61,"line":115},[59,2334,133],{"class":65},[59,2336,920],{"class":86},[59,2338,139],{"class":65},[59,2340,2341],{"class":69}," setInterval",[59,2343,2344],{"class":73},"(callback, delay) ",[59,2346,2347],{"class":227},"\u002F\u002F stale: callback changes, interval doesn't\n",[59,2349,2350,2352,2354,2356,2359],{"class":61,"line":130},[59,2351,174],{"class":65},[59,2353,142],{"class":73},[59,2355,124],{"class":65},[59,2357,2358],{"class":69}," clearInterval",[59,2360,2361],{"class":73},"(id)\n",[59,2363,2364],{"class":61,"line":152},[59,2365,2366],{"class":73},"  }, [delay])\n",[59,2368,2369],{"class":61,"line":171},[59,2370,216],{"class":73},[15,2372,2373],{},"Fix with the ref-mirror pattern:",[50,2375,2377],{"className":52,"code":2376,"language":54,"meta":55,"style":55},"function useInterval(callback, delay) {\n  const cbRef = useRef(callback)\n  useEffect(() => { cbRef.current = callback }) \u002F\u002F always current\n\n  useEffect(() => {\n    const id = setInterval(() => cbRef.current(), delay)\n    return () => clearInterval(id)\n  }, [delay])\n}\n",[26,2378,2379,2395,2409,2428,2432,2442,2465,2477,2481],{"__ignoreMap":55},[59,2380,2381,2383,2385,2387,2389,2391,2393],{"class":61,"line":62},[59,2382,66],{"class":65},[59,2384,2309],{"class":69},[59,2386,161],{"class":73},[59,2388,2314],{"class":425},[59,2390,90],{"class":73},[59,2392,869],{"class":425},[59,2394,434],{"class":73},[59,2396,2397,2399,2402,2404,2406],{"class":61,"line":77},[59,2398,80],{"class":65},[59,2400,2401],{"class":86}," cbRef",[59,2403,139],{"class":65},[59,2405,779],{"class":69},[59,2407,2408],{"class":73},"(callback)\n",[59,2410,2411,2413,2415,2417,2420,2422,2425],{"class":61,"line":108},[59,2412,118],{"class":69},[59,2414,121],{"class":73},[59,2416,124],{"class":65},[59,2418,2419],{"class":73}," { cbRef.current ",[59,2421,99],{"class":65},[59,2423,2424],{"class":73}," callback }) ",[59,2426,2427],{"class":227},"\u002F\u002F always current\n",[59,2429,2430],{"class":61,"line":115},[59,2431,112],{"emptyLinePlaceholder":111},[59,2433,2434,2436,2438,2440],{"class":61,"line":130},[59,2435,118],{"class":69},[59,2437,121],{"class":73},[59,2439,124],{"class":65},[59,2441,127],{"class":73},[59,2443,2444,2446,2448,2450,2452,2454,2456,2459,2462],{"class":61,"line":152},[59,2445,133],{"class":65},[59,2447,920],{"class":86},[59,2449,139],{"class":65},[59,2451,2341],{"class":69},[59,2453,121],{"class":73},[59,2455,124],{"class":65},[59,2457,2458],{"class":73}," cbRef.",[59,2460,2461],{"class":69},"current",[59,2463,2464],{"class":73},"(), delay)\n",[59,2466,2467,2469,2471,2473,2475],{"class":61,"line":171},[59,2468,174],{"class":65},[59,2470,142],{"class":73},[59,2472,124],{"class":65},[59,2474,2358],{"class":69},[59,2476,2361],{"class":73},[59,2478,2479],{"class":61,"line":193},[59,2480,2366],{"class":73},[59,2482,2483],{"class":61,"line":199},[59,2484,216],{"class":73},[10,2486,2488],{"id":2487},"common-interview-questions-at-a-glance","Common interview questions at a glance",[1926,2490,2491,2500,2509,2515,2521,2535],{},[1889,2492,2493,2496,2497,2499],{},[18,2494,2495],{},"What is a custom hook?"," A function whose name starts with ",[26,2498,47],{}," that calls one or\nmore React hooks — the primary pattern for sharing stateful logic.",[1889,2501,2502,2508],{},[18,2503,2504,2505,2507],{},"Why must it start with ",[26,2506,47],{},"?"," The linter uses the prefix to identify hooks and\napply the rules of hooks to the function's internals.",[1889,2510,2511,2514],{},[18,2512,2513],{},"How does it differ from a utility function?"," A utility function has no hooks and\ncan be called anywhere; a custom hook has hooks and must follow the rules of hooks.",[1889,2516,2517,2520],{},[18,2518,2519],{},"Does each caller get its own state?"," Yes — each component that calls a custom hook\ngets its own isolated state; hooks are not singletons.",[1889,2522,2523,2526,2527,2529,2530,2532,2533,262],{},[18,2524,2525],{},"How do you test a custom hook?"," Use ",[26,2528,1913],{}," from ",[26,2531,2036],{}," and\nwrap updates in ",[26,2534,2043],{},[1889,2536,2537,2540],{},[18,2538,2539],{},"When should you create a custom hook?"," When the same hook combination appears in\ntwo+ places, or when hook logic obscures the component's render output.",[2542,2543,2544],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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}",{"title":55,"searchDepth":77,"depth":77,"links":2546},[2547,2548,2549,2551,2552,2553,2560,2561,2562,2563,2564,2565],{"id":12,"depth":77,"text":13},{"id":36,"depth":77,"text":37},{"id":265,"depth":77,"text":2550},"Why the use prefix is required",{"id":390,"depth":77,"text":391},{"id":536,"depth":77,"text":537},{"id":739,"depth":77,"text":740,"children":2554},[2555,2556,2557,2558,2559],{"id":744,"depth":108,"text":745},{"id":845,"depth":108,"text":28},{"id":1017,"depth":108,"text":1018},{"id":1270,"depth":108,"text":1271},{"id":1483,"depth":108,"text":1484},{"id":1778,"depth":77,"text":1779},{"id":1880,"depth":77,"text":1881},{"id":1920,"depth":77,"text":1921},{"id":2029,"depth":77,"text":2030},{"id":2288,"depth":77,"text":2289},{"id":2487,"depth":77,"text":2488},"Master React custom hooks for interviews — naming rules, extracting shared logic, usePrevious, useDebounce, useLocalStorage, useMediaQuery, hook composition, and testing.","medium","md","React","react",{},"\u002Fblog\u002Freact-custom-hooks-guide","\u002Freact\u002Fhooks\u002Fcustom-hooks",{"title":5,"description":2566},"blog\u002Freact-custom-hooks-guide","Custom Hooks","Hooks","hooks","2026-06-23","5xjh8gorB9jFzEAX5WWr3of9-6kM84jBVl15dZiN4RY",1782244083427]