[{"data":1,"prerenderedAt":2382},["ShallowReactive",2],{"page-\u002Freact\u002F11-custom-hooks":3},{"id":4,"title":5,"body":6,"description":2375,"extension":2376,"meta":2377,"navigation":58,"path":2378,"seo":2379,"stem":2380,"__hash__":2381},"content\u002Freact\u002F11-custom-hooks.md","11 — Custom Hooks",{"type":7,"value":8,"toc":2361},"minimark",[9,13,18,101,105,110,464,468,955,959,1362,1366,1563,1567,1823,1827,1924,1928,2032,2036,2044,2125,2269,2273,2357],[10,11,5],"h1",{"id":12},"_11-custom-hooks",[14,15,17],"h2",{"id":16},"what-custom-hooks-are","What Custom Hooks Are",[19,20,23],"code-wrapper",{"filename":21,"language":22},"custom_hook_basics.js","javascript",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F A custom hook is a JavaScript function that starts with \"use\" and may call\n\u002F\u002F other hooks inside it. It's the React mechanism for REUSING STATEFUL LOGIC\n\u002F\u002F across components — not state itself, but the logic that manages state.\n\n\u002F\u002F The \"use\" prefix is NOT just convention — the linter (eslint-plugin-react-hooks)\n\u002F\u002F treats it as a signal that the function follows the Rules of Hooks. Without\n\u002F\u002F the \"use\" prefix, the linter won't check it, and hook violations go undetected.\n\n\u002F\u002F RULES OF HOOKS (enforced inside custom hooks):\n\u002F\u002F 1. Only call hooks at the TOP LEVEL — not inside loops, conditions, or nested functions\n\u002F\u002F 2. Only call hooks from React functions (components or other custom hooks)\n","",[30,31,32,41,47,53,60,66,72,78,83,89,95],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","\u002F\u002F A custom hook is a JavaScript function that starts with \"use\" and may call\n",[33,42,44],{"class":35,"line":43},2,[33,45,46],{"class":39},"\u002F\u002F other hooks inside it. It's the React mechanism for REUSING STATEFUL LOGIC\n",[33,48,50],{"class":35,"line":49},3,[33,51,52],{"class":39},"\u002F\u002F across components — not state itself, but the logic that manages state.\n",[33,54,56],{"class":35,"line":55},4,[33,57,59],{"emptyLinePlaceholder":58},true,"\n",[33,61,63],{"class":35,"line":62},5,[33,64,65],{"class":39},"\u002F\u002F The \"use\" prefix is NOT just convention — the linter (eslint-plugin-react-hooks)\n",[33,67,69],{"class":35,"line":68},6,[33,70,71],{"class":39},"\u002F\u002F treats it as a signal that the function follows the Rules of Hooks. Without\n",[33,73,75],{"class":35,"line":74},7,[33,76,77],{"class":39},"\u002F\u002F the \"use\" prefix, the linter won't check it, and hook violations go undetected.\n",[33,79,81],{"class":35,"line":80},8,[33,82,59],{"emptyLinePlaceholder":58},[33,84,86],{"class":35,"line":85},9,[33,87,88],{"class":39},"\u002F\u002F RULES OF HOOKS (enforced inside custom hooks):\n",[33,90,92],{"class":35,"line":91},10,[33,93,94],{"class":39},"\u002F\u002F 1. Only call hooks at the TOP LEVEL — not inside loops, conditions, or nested functions\n",[33,96,98],{"class":35,"line":97},11,[33,99,100],{"class":39},"\u002F\u002F 2. Only call hooks from React functions (components or other custom hooks)\n",[14,102,104],{"id":103},"production-hooks","Production Hooks",[106,107,109],"h3",{"id":108},"usedebounce","useDebounce",[19,111,113],{"filename":112,"language":22},"useDebounce.js",[24,114,116],{"className":26,"code":115,"language":22,"meta":28,"style":28},"import { useState, useEffect } from 'react'\n\nfunction useDebounce(value, delay = 300) {\n  const [debouncedValue, setDebouncedValue] = useState(value)\n\n  useEffect(() => {\n    const timeoutId = setTimeout(() => setDebouncedValue(value), delay)\n    return () => clearTimeout(timeoutId)\n    \u002F\u002F Cleanup on every value change: resets the timer.\n    \u002F\u002F The consumer only sees the debounced value after the user STOPS\n    \u002F\u002F changing the input for `delay` ms.\n  }, [value, delay])\n\n  return debouncedValue\n}\n\n\u002F\u002F Usage:\nfunction SearchPage() {\n  const [query, setQuery] = useState('')\n  const debouncedQuery = useDebounce(query, 500)\n  \u002F\u002F debouncedQuery lags behind query by 500ms of inactivity\n\n  useEffect(() => {\n    if (debouncedQuery) fetchResults(debouncedQuery)\n  }, [debouncedQuery])  \u002F\u002F API call only fires when user stops typing for 500ms\n\n  return \u003Cinput value={query} onChange={e => setQuery(e.target.value)} \u002F>\n}\n",[30,117,118,135,139,171,199,203,217,240,256,261,266,271,277,282,291,297,302,308,319,348,368,374,379,390,405,414,419,459],{"__ignoreMap":28},[33,119,120,124,128,131],{"class":35,"line":36},[33,121,123],{"class":122},"svdQ7","import",[33,125,127],{"class":126},"ssxIu"," { useState, useEffect } ",[33,129,130],{"class":122},"from",[33,132,134],{"class":133},"sJ6F3"," 'react'\n",[33,136,137],{"class":35,"line":43},[33,138,59],{"emptyLinePlaceholder":58},[33,140,141,144,148,151,155,158,161,164,168],{"class":35,"line":49},[33,142,143],{"class":122},"function",[33,145,147],{"class":146},"sIsaT"," useDebounce",[33,149,150],{"class":126},"(",[33,152,154],{"class":153},"sCrzJ","value",[33,156,157],{"class":126},", ",[33,159,160],{"class":153},"delay",[33,162,163],{"class":122}," =",[33,165,167],{"class":166},"snvgF"," 300",[33,169,170],{"class":126},") {\n",[33,172,173,176,179,182,184,187,190,193,196],{"class":35,"line":55},[33,174,175],{"class":122},"  const",[33,177,178],{"class":126}," [",[33,180,181],{"class":166},"debouncedValue",[33,183,157],{"class":126},[33,185,186],{"class":166},"setDebouncedValue",[33,188,189],{"class":126},"] ",[33,191,192],{"class":122},"=",[33,194,195],{"class":146}," useState",[33,197,198],{"class":126},"(value)\n",[33,200,201],{"class":35,"line":62},[33,202,59],{"emptyLinePlaceholder":58},[33,204,205,208,211,214],{"class":35,"line":68},[33,206,207],{"class":146},"  useEffect",[33,209,210],{"class":126},"(() ",[33,212,213],{"class":122},"=>",[33,215,216],{"class":126}," {\n",[33,218,219,222,225,227,230,232,234,237],{"class":35,"line":74},[33,220,221],{"class":122},"    const",[33,223,224],{"class":166}," timeoutId",[33,226,163],{"class":122},[33,228,229],{"class":146}," setTimeout",[33,231,210],{"class":126},[33,233,213],{"class":122},[33,235,236],{"class":146}," setDebouncedValue",[33,238,239],{"class":126},"(value), delay)\n",[33,241,242,245,248,250,253],{"class":35,"line":80},[33,243,244],{"class":122},"    return",[33,246,247],{"class":126}," () ",[33,249,213],{"class":122},[33,251,252],{"class":146}," clearTimeout",[33,254,255],{"class":126},"(timeoutId)\n",[33,257,258],{"class":35,"line":85},[33,259,260],{"class":39},"    \u002F\u002F Cleanup on every value change: resets the timer.\n",[33,262,263],{"class":35,"line":91},[33,264,265],{"class":39},"    \u002F\u002F The consumer only sees the debounced value after the user STOPS\n",[33,267,268],{"class":35,"line":97},[33,269,270],{"class":39},"    \u002F\u002F changing the input for `delay` ms.\n",[33,272,274],{"class":35,"line":273},12,[33,275,276],{"class":126},"  }, [value, delay])\n",[33,278,280],{"class":35,"line":279},13,[33,281,59],{"emptyLinePlaceholder":58},[33,283,285,288],{"class":35,"line":284},14,[33,286,287],{"class":122},"  return",[33,289,290],{"class":126}," debouncedValue\n",[33,292,294],{"class":35,"line":293},15,[33,295,296],{"class":126},"}\n",[33,298,300],{"class":35,"line":299},16,[33,301,59],{"emptyLinePlaceholder":58},[33,303,305],{"class":35,"line":304},17,[33,306,307],{"class":39},"\u002F\u002F Usage:\n",[33,309,311,313,316],{"class":35,"line":310},18,[33,312,143],{"class":122},[33,314,315],{"class":146}," SearchPage",[33,317,318],{"class":126},"() {\n",[33,320,322,324,326,329,331,334,336,338,340,342,345],{"class":35,"line":321},19,[33,323,175],{"class":122},[33,325,178],{"class":126},[33,327,328],{"class":166},"query",[33,330,157],{"class":126},[33,332,333],{"class":166},"setQuery",[33,335,189],{"class":126},[33,337,192],{"class":122},[33,339,195],{"class":146},[33,341,150],{"class":126},[33,343,344],{"class":133},"''",[33,346,347],{"class":126},")\n",[33,349,351,353,356,358,360,363,366],{"class":35,"line":350},20,[33,352,175],{"class":122},[33,354,355],{"class":166}," debouncedQuery",[33,357,163],{"class":122},[33,359,147],{"class":146},[33,361,362],{"class":126},"(query, ",[33,364,365],{"class":166},"500",[33,367,347],{"class":126},[33,369,371],{"class":35,"line":370},21,[33,372,373],{"class":39},"  \u002F\u002F debouncedQuery lags behind query by 500ms of inactivity\n",[33,375,377],{"class":35,"line":376},22,[33,378,59],{"emptyLinePlaceholder":58},[33,380,382,384,386,388],{"class":35,"line":381},23,[33,383,207],{"class":146},[33,385,210],{"class":126},[33,387,213],{"class":122},[33,389,216],{"class":126},[33,391,393,396,399,402],{"class":35,"line":392},24,[33,394,395],{"class":122},"    if",[33,397,398],{"class":126}," (debouncedQuery) ",[33,400,401],{"class":146},"fetchResults",[33,403,404],{"class":126},"(debouncedQuery)\n",[33,406,408,411],{"class":35,"line":407},25,[33,409,410],{"class":126},"  }, [debouncedQuery])  ",[33,412,413],{"class":39},"\u002F\u002F API call only fires when user stops typing for 500ms\n",[33,415,417],{"class":35,"line":416},26,[33,418,59],{"emptyLinePlaceholder":58},[33,420,422,424,427,431,434,436,439,442,444,447,450,453,456],{"class":35,"line":421},27,[33,423,287],{"class":122},[33,425,426],{"class":126}," \u003C",[33,428,430],{"class":429},"sk71V","input",[33,432,433],{"class":146}," value",[33,435,192],{"class":122},[33,437,438],{"class":126},"{query} ",[33,440,441],{"class":146},"onChange",[33,443,192],{"class":122},[33,445,446],{"class":126},"{",[33,448,449],{"class":153},"e",[33,451,452],{"class":122}," =>",[33,454,455],{"class":146}," setQuery",[33,457,458],{"class":126},"(e.target.value)} \u002F>\n",[33,460,462],{"class":35,"line":461},28,[33,463,296],{"class":126},[106,465,467],{"id":466},"usefetch","useFetch",[19,469,471],{"filename":470,"language":22},"useFetch.js",[24,472,474],{"className":26,"code":473,"language":22,"meta":28,"style":28},"import { useReducer, useEffect } from 'react'\n\nconst initialState = { data: null, loading: true, error: null }\n\nfunction fetchReducer(state, action) {\n  switch (action.type) {\n    case 'start': return { data: null, loading: true, error: null }\n    case 'success': return { data: action.data, loading: false, error: null }\n    case 'error': return { data: null, loading: false, error: action.error }\n    default: return state\n  }\n}\n\nfunction useFetch(url, options) {\n  const [state, dispatch] = useReducer(fetchReducer, initialState)\n\n  useEffect(() => {\n    if (!url) return\n    const controller = new AbortController()\n    dispatch({ type: 'start' })\n    fetch(url, { ...options, signal: controller.signal })\n      .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json() })\n      .then(data => dispatch({ type: 'success', data }))\n      .catch(err => { if (err.name !== 'AbortError') dispatch({ type: 'error', error: err.message }) })\n    return () => controller.abort()\n  }, [url])  \u002F\u002F only re-fetch when url changes — NOT when options changes\n  \u002F\u002F CAVEAT: options is usually a new object every render → including it in deps\n  \u002F\u002F would cause an infinite fetch loop. The caller must memoize options if they\n  \u002F\u002F want re-fetching on option changes, or use a JSON stringified dep.\n\n  return state\n}\n",[30,475,476,487,491,521,525,544,552,580,603,625,637,642,646,650,669,692,696,706,722,740,754,768,834,858,898,914,922,927,932,938,943,950],{"__ignoreMap":28},[33,477,478,480,483,485],{"class":35,"line":36},[33,479,123],{"class":122},[33,481,482],{"class":126}," { useReducer, useEffect } ",[33,484,130],{"class":122},[33,486,134],{"class":133},[33,488,489],{"class":35,"line":43},[33,490,59],{"emptyLinePlaceholder":58},[33,492,493,496,499,501,504,507,510,513,516,518],{"class":35,"line":49},[33,494,495],{"class":122},"const",[33,497,498],{"class":166}," initialState",[33,500,163],{"class":122},[33,502,503],{"class":126}," { data: ",[33,505,506],{"class":166},"null",[33,508,509],{"class":126},", loading: ",[33,511,512],{"class":166},"true",[33,514,515],{"class":126},", error: ",[33,517,506],{"class":166},[33,519,520],{"class":126}," }\n",[33,522,523],{"class":35,"line":55},[33,524,59],{"emptyLinePlaceholder":58},[33,526,527,529,532,534,537,539,542],{"class":35,"line":62},[33,528,143],{"class":122},[33,530,531],{"class":146}," fetchReducer",[33,533,150],{"class":126},[33,535,536],{"class":153},"state",[33,538,157],{"class":126},[33,540,541],{"class":153},"action",[33,543,170],{"class":126},[33,545,546,549],{"class":35,"line":68},[33,547,548],{"class":122},"  switch",[33,550,551],{"class":126}," (action.type) {\n",[33,553,554,557,560,563,566,568,570,572,574,576,578],{"class":35,"line":74},[33,555,556],{"class":122},"    case",[33,558,559],{"class":133}," 'start'",[33,561,562],{"class":126},": ",[33,564,565],{"class":122},"return",[33,567,503],{"class":126},[33,569,506],{"class":166},[33,571,509],{"class":126},[33,573,512],{"class":166},[33,575,515],{"class":126},[33,577,506],{"class":166},[33,579,520],{"class":126},[33,581,582,584,587,589,591,594,597,599,601],{"class":35,"line":80},[33,583,556],{"class":122},[33,585,586],{"class":133}," 'success'",[33,588,562],{"class":126},[33,590,565],{"class":122},[33,592,593],{"class":126}," { data: action.data, loading: ",[33,595,596],{"class":166},"false",[33,598,515],{"class":126},[33,600,506],{"class":166},[33,602,520],{"class":126},[33,604,605,607,610,612,614,616,618,620,622],{"class":35,"line":85},[33,606,556],{"class":122},[33,608,609],{"class":133}," 'error'",[33,611,562],{"class":126},[33,613,565],{"class":122},[33,615,503],{"class":126},[33,617,506],{"class":166},[33,619,509],{"class":126},[33,621,596],{"class":166},[33,623,624],{"class":126},", error: action.error }\n",[33,626,627,630,632,634],{"class":35,"line":91},[33,628,629],{"class":122},"    default",[33,631,562],{"class":126},[33,633,565],{"class":122},[33,635,636],{"class":126}," state\n",[33,638,639],{"class":35,"line":97},[33,640,641],{"class":126},"  }\n",[33,643,644],{"class":35,"line":273},[33,645,296],{"class":126},[33,647,648],{"class":35,"line":279},[33,649,59],{"emptyLinePlaceholder":58},[33,651,652,654,657,659,662,664,667],{"class":35,"line":284},[33,653,143],{"class":122},[33,655,656],{"class":146}," useFetch",[33,658,150],{"class":126},[33,660,661],{"class":153},"url",[33,663,157],{"class":126},[33,665,666],{"class":153},"options",[33,668,170],{"class":126},[33,670,671,673,675,677,679,682,684,686,689],{"class":35,"line":293},[33,672,175],{"class":122},[33,674,178],{"class":126},[33,676,536],{"class":166},[33,678,157],{"class":126},[33,680,681],{"class":166},"dispatch",[33,683,189],{"class":126},[33,685,192],{"class":122},[33,687,688],{"class":146}," useReducer",[33,690,691],{"class":126},"(fetchReducer, initialState)\n",[33,693,694],{"class":35,"line":299},[33,695,59],{"emptyLinePlaceholder":58},[33,697,698,700,702,704],{"class":35,"line":304},[33,699,207],{"class":146},[33,701,210],{"class":126},[33,703,213],{"class":122},[33,705,216],{"class":126},[33,707,708,710,713,716,719],{"class":35,"line":310},[33,709,395],{"class":122},[33,711,712],{"class":126}," (",[33,714,715],{"class":122},"!",[33,717,718],{"class":126},"url) ",[33,720,721],{"class":122},"return\n",[33,723,724,726,729,731,734,737],{"class":35,"line":321},[33,725,221],{"class":122},[33,727,728],{"class":166}," controller",[33,730,163],{"class":122},[33,732,733],{"class":122}," new",[33,735,736],{"class":146}," AbortController",[33,738,739],{"class":126},"()\n",[33,741,742,745,748,751],{"class":35,"line":350},[33,743,744],{"class":146},"    dispatch",[33,746,747],{"class":126},"({ type: ",[33,749,750],{"class":133},"'start'",[33,752,753],{"class":126}," })\n",[33,755,756,759,762,765],{"class":35,"line":370},[33,757,758],{"class":146},"    fetch",[33,760,761],{"class":126},"(url, { ",[33,763,764],{"class":122},"...",[33,766,767],{"class":126},"options, signal: controller.signal })\n",[33,769,770,773,776,778,781,783,786,789,791,793,796,799,801,804,806,809,811,814,817,820,823,825,828,831],{"class":35,"line":376},[33,771,772],{"class":126},"      .",[33,774,775],{"class":146},"then",[33,777,150],{"class":126},[33,779,780],{"class":153},"res",[33,782,452],{"class":122},[33,784,785],{"class":126}," { ",[33,787,788],{"class":122},"if",[33,790,712],{"class":126},[33,792,715],{"class":122},[33,794,795],{"class":126},"res.ok) ",[33,797,798],{"class":122},"throw",[33,800,733],{"class":122},[33,802,803],{"class":146}," Error",[33,805,150],{"class":126},[33,807,808],{"class":133},"`HTTP ${",[33,810,780],{"class":126},[33,812,813],{"class":133},".",[33,815,816],{"class":126},"status",[33,818,819],{"class":133},"}`",[33,821,822],{"class":126},"); ",[33,824,565],{"class":122},[33,826,827],{"class":126}," res.",[33,829,830],{"class":146},"json",[33,832,833],{"class":126},"() })\n",[33,835,836,838,840,842,845,847,850,852,855],{"class":35,"line":381},[33,837,772],{"class":126},[33,839,775],{"class":146},[33,841,150],{"class":126},[33,843,844],{"class":153},"data",[33,846,452],{"class":122},[33,848,849],{"class":146}," dispatch",[33,851,747],{"class":126},[33,853,854],{"class":133},"'success'",[33,856,857],{"class":126},", data }))\n",[33,859,860,862,865,867,870,872,874,876,879,882,885,888,890,892,895],{"class":35,"line":392},[33,861,772],{"class":126},[33,863,864],{"class":146},"catch",[33,866,150],{"class":126},[33,868,869],{"class":153},"err",[33,871,452],{"class":122},[33,873,785],{"class":126},[33,875,788],{"class":122},[33,877,878],{"class":126}," (err.name ",[33,880,881],{"class":122},"!==",[33,883,884],{"class":133}," 'AbortError'",[33,886,887],{"class":126},") ",[33,889,681],{"class":146},[33,891,747],{"class":126},[33,893,894],{"class":133},"'error'",[33,896,897],{"class":126},", error: err.message }) })\n",[33,899,900,902,904,906,909,912],{"class":35,"line":407},[33,901,244],{"class":122},[33,903,247],{"class":126},[33,905,213],{"class":122},[33,907,908],{"class":126}," controller.",[33,910,911],{"class":146},"abort",[33,913,739],{"class":126},[33,915,916,919],{"class":35,"line":416},[33,917,918],{"class":126},"  }, [url])  ",[33,920,921],{"class":39},"\u002F\u002F only re-fetch when url changes — NOT when options changes\n",[33,923,924],{"class":35,"line":421},[33,925,926],{"class":39},"  \u002F\u002F CAVEAT: options is usually a new object every render → including it in deps\n",[33,928,929],{"class":35,"line":461},[33,930,931],{"class":39},"  \u002F\u002F would cause an infinite fetch loop. The caller must memoize options if they\n",[33,933,935],{"class":35,"line":934},29,[33,936,937],{"class":39},"  \u002F\u002F want re-fetching on option changes, or use a JSON stringified dep.\n",[33,939,941],{"class":35,"line":940},30,[33,942,59],{"emptyLinePlaceholder":58},[33,944,946,948],{"class":35,"line":945},31,[33,947,287],{"class":122},[33,949,636],{"class":126},[33,951,953],{"class":35,"line":952},32,[33,954,296],{"class":126},[106,956,958],{"id":957},"uselocalstorage","useLocalStorage",[19,960,962],{"filename":961,"language":22},"useLocalStorage.js",[24,963,965],{"className":26,"code":964,"language":22,"meta":28,"style":28},"import { useState, useEffect, useCallback } from 'react'\n\nfunction useLocalStorage(key, initialValue) {\n  \u002F\u002F Lazy init: read from localStorage only on mount\n  const [value, setValue] = useState(() => {\n    try {\n      const stored = localStorage.getItem(key)\n      return stored ? JSON.parse(stored) : initialValue\n    } catch {\n      return initialValue  \u002F\u002F localStorage might be unavailable (SSR, privacy mode)\n    }\n  })\n\n  \u002F\u002F Persist to localStorage whenever value changes\n  useEffect(() => {\n    try {\n      localStorage.setItem(key, JSON.stringify(value))\n    } catch (err) {\n      console.warn(`Failed to persist ${key}:`, err)\n    }\n  }, [key, value])\n\n  \u002F\u002F Listen for cross-tab changes (storage event fires in other tabs)\n  useEffect(() => {\n    const handleStorageChange = (e) => {\n      if (e.key === key && e.newValue !== null) {\n        try { setValue(JSON.parse(e.newValue)) } catch { \u002F* ignore parse errors *\u002F }\n      }\n    }\n    window.addEventListener('storage', handleStorageChange)\n    return () => window.removeEventListener('storage', handleStorageChange)\n  }, [key])\n\n  return [value, setValue]\n}\n",[30,966,967,978,982,1001,1006,1031,1038,1057,1085,1094,1104,1109,1114,1118,1123,1133,1139,1161,1170,1191,1195,1200,1204,1209,1219,1238,1265,1294,1299,1303,1319,1339,1344,1349,1357],{"__ignoreMap":28},[33,968,969,971,974,976],{"class":35,"line":36},[33,970,123],{"class":122},[33,972,973],{"class":126}," { useState, useEffect, useCallback } ",[33,975,130],{"class":122},[33,977,134],{"class":133},[33,979,980],{"class":35,"line":43},[33,981,59],{"emptyLinePlaceholder":58},[33,983,984,986,989,991,994,996,999],{"class":35,"line":49},[33,985,143],{"class":122},[33,987,988],{"class":146}," useLocalStorage",[33,990,150],{"class":126},[33,992,993],{"class":153},"key",[33,995,157],{"class":126},[33,997,998],{"class":153},"initialValue",[33,1000,170],{"class":126},[33,1002,1003],{"class":35,"line":55},[33,1004,1005],{"class":39},"  \u002F\u002F Lazy init: read from localStorage only on mount\n",[33,1007,1008,1010,1012,1014,1016,1019,1021,1023,1025,1027,1029],{"class":35,"line":62},[33,1009,175],{"class":122},[33,1011,178],{"class":126},[33,1013,154],{"class":166},[33,1015,157],{"class":126},[33,1017,1018],{"class":166},"setValue",[33,1020,189],{"class":126},[33,1022,192],{"class":122},[33,1024,195],{"class":146},[33,1026,210],{"class":126},[33,1028,213],{"class":122},[33,1030,216],{"class":126},[33,1032,1033,1036],{"class":35,"line":68},[33,1034,1035],{"class":122},"    try",[33,1037,216],{"class":126},[33,1039,1040,1043,1046,1048,1051,1054],{"class":35,"line":74},[33,1041,1042],{"class":122},"      const",[33,1044,1045],{"class":166}," stored",[33,1047,163],{"class":122},[33,1049,1050],{"class":126}," localStorage.",[33,1052,1053],{"class":146},"getItem",[33,1055,1056],{"class":126},"(key)\n",[33,1058,1059,1062,1065,1068,1071,1073,1076,1079,1082],{"class":35,"line":80},[33,1060,1061],{"class":122},"      return",[33,1063,1064],{"class":126}," stored ",[33,1066,1067],{"class":122},"?",[33,1069,1070],{"class":166}," JSON",[33,1072,813],{"class":126},[33,1074,1075],{"class":146},"parse",[33,1077,1078],{"class":126},"(stored) ",[33,1080,1081],{"class":122},":",[33,1083,1084],{"class":126}," initialValue\n",[33,1086,1087,1090,1092],{"class":35,"line":85},[33,1088,1089],{"class":126},"    } ",[33,1091,864],{"class":122},[33,1093,216],{"class":126},[33,1095,1096,1098,1101],{"class":35,"line":91},[33,1097,1061],{"class":122},[33,1099,1100],{"class":126}," initialValue  ",[33,1102,1103],{"class":39},"\u002F\u002F localStorage might be unavailable (SSR, privacy mode)\n",[33,1105,1106],{"class":35,"line":97},[33,1107,1108],{"class":126},"    }\n",[33,1110,1111],{"class":35,"line":273},[33,1112,1113],{"class":126},"  })\n",[33,1115,1116],{"class":35,"line":279},[33,1117,59],{"emptyLinePlaceholder":58},[33,1119,1120],{"class":35,"line":284},[33,1121,1122],{"class":39},"  \u002F\u002F Persist to localStorage whenever value changes\n",[33,1124,1125,1127,1129,1131],{"class":35,"line":293},[33,1126,207],{"class":146},[33,1128,210],{"class":126},[33,1130,213],{"class":122},[33,1132,216],{"class":126},[33,1134,1135,1137],{"class":35,"line":299},[33,1136,1035],{"class":122},[33,1138,216],{"class":126},[33,1140,1141,1144,1147,1150,1153,1155,1158],{"class":35,"line":304},[33,1142,1143],{"class":126},"      localStorage.",[33,1145,1146],{"class":146},"setItem",[33,1148,1149],{"class":126},"(key, ",[33,1151,1152],{"class":166},"JSON",[33,1154,813],{"class":126},[33,1156,1157],{"class":146},"stringify",[33,1159,1160],{"class":126},"(value))\n",[33,1162,1163,1165,1167],{"class":35,"line":310},[33,1164,1089],{"class":126},[33,1166,864],{"class":122},[33,1168,1169],{"class":126}," (err) {\n",[33,1171,1172,1175,1178,1180,1183,1185,1188],{"class":35,"line":321},[33,1173,1174],{"class":126},"      console.",[33,1176,1177],{"class":146},"warn",[33,1179,150],{"class":126},[33,1181,1182],{"class":133},"`Failed to persist ${",[33,1184,993],{"class":126},[33,1186,1187],{"class":133},"}:`",[33,1189,1190],{"class":126},", err)\n",[33,1192,1193],{"class":35,"line":350},[33,1194,1108],{"class":126},[33,1196,1197],{"class":35,"line":370},[33,1198,1199],{"class":126},"  }, [key, value])\n",[33,1201,1202],{"class":35,"line":376},[33,1203,59],{"emptyLinePlaceholder":58},[33,1205,1206],{"class":35,"line":381},[33,1207,1208],{"class":39},"  \u002F\u002F Listen for cross-tab changes (storage event fires in other tabs)\n",[33,1210,1211,1213,1215,1217],{"class":35,"line":392},[33,1212,207],{"class":146},[33,1214,210],{"class":126},[33,1216,213],{"class":122},[33,1218,216],{"class":126},[33,1220,1221,1223,1226,1228,1230,1232,1234,1236],{"class":35,"line":407},[33,1222,221],{"class":122},[33,1224,1225],{"class":146}," handleStorageChange",[33,1227,163],{"class":122},[33,1229,712],{"class":126},[33,1231,449],{"class":153},[33,1233,887],{"class":126},[33,1235,213],{"class":122},[33,1237,216],{"class":126},[33,1239,1240,1243,1246,1249,1252,1255,1258,1260,1263],{"class":35,"line":416},[33,1241,1242],{"class":122},"      if",[33,1244,1245],{"class":126}," (e.key ",[33,1247,1248],{"class":122},"===",[33,1250,1251],{"class":126}," key ",[33,1253,1254],{"class":122},"&&",[33,1256,1257],{"class":126}," e.newValue ",[33,1259,881],{"class":122},[33,1261,1262],{"class":166}," null",[33,1264,170],{"class":126},[33,1266,1267,1270,1272,1274,1276,1278,1280,1282,1285,1287,1289,1292],{"class":35,"line":421},[33,1268,1269],{"class":122},"        try",[33,1271,785],{"class":126},[33,1273,1018],{"class":146},[33,1275,150],{"class":126},[33,1277,1152],{"class":166},[33,1279,813],{"class":126},[33,1281,1075],{"class":146},[33,1283,1284],{"class":126},"(e.newValue)) } ",[33,1286,864],{"class":122},[33,1288,785],{"class":126},[33,1290,1291],{"class":39},"\u002F* ignore parse errors *\u002F",[33,1293,520],{"class":126},[33,1295,1296],{"class":35,"line":461},[33,1297,1298],{"class":126},"      }\n",[33,1300,1301],{"class":35,"line":934},[33,1302,1108],{"class":126},[33,1304,1305,1308,1311,1313,1316],{"class":35,"line":940},[33,1306,1307],{"class":126},"    window.",[33,1309,1310],{"class":146},"addEventListener",[33,1312,150],{"class":126},[33,1314,1315],{"class":133},"'storage'",[33,1317,1318],{"class":126},", handleStorageChange)\n",[33,1320,1321,1323,1325,1327,1330,1333,1335,1337],{"class":35,"line":945},[33,1322,244],{"class":122},[33,1324,247],{"class":126},[33,1326,213],{"class":122},[33,1328,1329],{"class":126}," window.",[33,1331,1332],{"class":146},"removeEventListener",[33,1334,150],{"class":126},[33,1336,1315],{"class":133},[33,1338,1318],{"class":126},[33,1340,1341],{"class":35,"line":952},[33,1342,1343],{"class":126},"  }, [key])\n",[33,1345,1347],{"class":35,"line":1346},33,[33,1348,59],{"emptyLinePlaceholder":58},[33,1350,1352,1354],{"class":35,"line":1351},34,[33,1353,287],{"class":122},[33,1355,1356],{"class":126}," [value, setValue]\n",[33,1358,1360],{"class":35,"line":1359},35,[33,1361,296],{"class":126},[106,1363,1365],{"id":1364},"useprevious","usePrevious",[19,1367,1369],{"filename":1368,"language":22},"usePrevious.js",[24,1370,1372],{"className":26,"code":1371,"language":22,"meta":28,"style":28},"import { useRef, useEffect } from 'react'\n\nfunction usePrevious(value) {\n  const ref = useRef()\n  \u002F\u002F Update the ref AFTER render so the CURRENT render still sees the old value.\n  \u002F\u002F The effect runs after paint → ref.current holds the value from the PREVIOUS render.\n  useEffect(() => {\n    ref.current = value\n  }, [value])\n  return ref.current\n}\n\n\u002F\u002F Usage:\nfunction PriceTag({ price }) {\n  const prevPrice = usePrevious(price)\n  const trend = prevPrice \u003C price ? '↑' : prevPrice > price ? '↓' : '→'\n  return \u003Cspan>{price} {trend}\u003C\u002Fspan>\n}\n",[30,1373,1374,1385,1389,1402,1416,1421,1426,1436,1446,1451,1458,1462,1466,1470,1486,1500,1543,1559],{"__ignoreMap":28},[33,1375,1376,1378,1381,1383],{"class":35,"line":36},[33,1377,123],{"class":122},[33,1379,1380],{"class":126}," { useRef, useEffect } ",[33,1382,130],{"class":122},[33,1384,134],{"class":133},[33,1386,1387],{"class":35,"line":43},[33,1388,59],{"emptyLinePlaceholder":58},[33,1390,1391,1393,1396,1398,1400],{"class":35,"line":49},[33,1392,143],{"class":122},[33,1394,1395],{"class":146}," usePrevious",[33,1397,150],{"class":126},[33,1399,154],{"class":153},[33,1401,170],{"class":126},[33,1403,1404,1406,1409,1411,1414],{"class":35,"line":55},[33,1405,175],{"class":122},[33,1407,1408],{"class":166}," ref",[33,1410,163],{"class":122},[33,1412,1413],{"class":146}," useRef",[33,1415,739],{"class":126},[33,1417,1418],{"class":35,"line":62},[33,1419,1420],{"class":39},"  \u002F\u002F Update the ref AFTER render so the CURRENT render still sees the old value.\n",[33,1422,1423],{"class":35,"line":68},[33,1424,1425],{"class":39},"  \u002F\u002F The effect runs after paint → ref.current holds the value from the PREVIOUS render.\n",[33,1427,1428,1430,1432,1434],{"class":35,"line":74},[33,1429,207],{"class":146},[33,1431,210],{"class":126},[33,1433,213],{"class":122},[33,1435,216],{"class":126},[33,1437,1438,1441,1443],{"class":35,"line":80},[33,1439,1440],{"class":126},"    ref.current ",[33,1442,192],{"class":122},[33,1444,1445],{"class":126}," value\n",[33,1447,1448],{"class":35,"line":85},[33,1449,1450],{"class":126},"  }, [value])\n",[33,1452,1453,1455],{"class":35,"line":91},[33,1454,287],{"class":122},[33,1456,1457],{"class":126}," ref.current\n",[33,1459,1460],{"class":35,"line":97},[33,1461,296],{"class":126},[33,1463,1464],{"class":35,"line":273},[33,1465,59],{"emptyLinePlaceholder":58},[33,1467,1468],{"class":35,"line":279},[33,1469,307],{"class":39},[33,1471,1472,1474,1477,1480,1483],{"class":35,"line":284},[33,1473,143],{"class":122},[33,1475,1476],{"class":146}," PriceTag",[33,1478,1479],{"class":126},"({ ",[33,1481,1482],{"class":153},"price",[33,1484,1485],{"class":126}," }) {\n",[33,1487,1488,1490,1493,1495,1497],{"class":35,"line":293},[33,1489,175],{"class":122},[33,1491,1492],{"class":166}," prevPrice",[33,1494,163],{"class":122},[33,1496,1395],{"class":146},[33,1498,1499],{"class":126},"(price)\n",[33,1501,1502,1504,1507,1509,1512,1515,1518,1520,1523,1526,1528,1531,1533,1535,1538,1540],{"class":35,"line":299},[33,1503,175],{"class":122},[33,1505,1506],{"class":166}," trend",[33,1508,163],{"class":122},[33,1510,1511],{"class":126}," prevPrice ",[33,1513,1514],{"class":122},"\u003C",[33,1516,1517],{"class":126}," price ",[33,1519,1067],{"class":122},[33,1521,1522],{"class":133}," '↑'",[33,1524,1525],{"class":122}," :",[33,1527,1511],{"class":126},[33,1529,1530],{"class":122},">",[33,1532,1517],{"class":126},[33,1534,1067],{"class":122},[33,1536,1537],{"class":133}," '↓'",[33,1539,1525],{"class":122},[33,1541,1542],{"class":133}," '→'\n",[33,1544,1545,1547,1549,1551,1554,1556],{"class":35,"line":304},[33,1546,287],{"class":122},[33,1548,426],{"class":126},[33,1550,33],{"class":429},[33,1552,1553],{"class":126},">{price} {trend}\u003C\u002F",[33,1555,33],{"class":429},[33,1557,1558],{"class":126},">\n",[33,1560,1561],{"class":35,"line":310},[33,1562,296],{"class":126},[14,1564,1566],{"id":1565},"hook-composition","Hook Composition",[19,1568,1570],{"filename":1569,"language":22},"hook_composition.js",[24,1571,1573],{"className":26,"code":1572,"language":22,"meta":28,"style":28},"\u002F\u002F Custom hooks can call other custom hooks — this is the primary mechanism for\n\u002F\u002F composing stateful logic. Build complex hooks from simpler ones.\n\nfunction useUser() {\n  const { data: user, loading } = useFetch('\u002Fapi\u002Fme')\n  const prevUser = usePrevious(user)\n  return { user, prevUser, loading }\n}\n\nfunction useAuth() {\n  const { user, loading } = useUser()\n  const [token, setToken] = useLocalStorage('authToken', null)\n  const isAuthenticated = Boolean(user && token)\n\n  const logout = useCallback(() => {\n    setToken(null)\n    fetch('\u002Fapi\u002Flogout', { method: 'POST' })\n  }, [setToken])\n\n  return { user, loading, isAuthenticated, token, setToken, logout }\n}\n\n\u002F\u002F This composition is what makes hooks powerful: complex logic built from\n\u002F\u002F simple, testable, reusable pieces — without inheritance or render props.\n",[30,1574,1575,1580,1585,1589,1598,1630,1644,1651,1655,1659,1668,1688,1719,1739,1743,1761,1772,1789,1794,1798,1805,1809,1813,1818],{"__ignoreMap":28},[33,1576,1577],{"class":35,"line":36},[33,1578,1579],{"class":39},"\u002F\u002F Custom hooks can call other custom hooks — this is the primary mechanism for\n",[33,1581,1582],{"class":35,"line":43},[33,1583,1584],{"class":39},"\u002F\u002F composing stateful logic. Build complex hooks from simpler ones.\n",[33,1586,1587],{"class":35,"line":49},[33,1588,59],{"emptyLinePlaceholder":58},[33,1590,1591,1593,1596],{"class":35,"line":55},[33,1592,143],{"class":122},[33,1594,1595],{"class":146}," useUser",[33,1597,318],{"class":126},[33,1599,1600,1602,1604,1606,1608,1611,1613,1616,1619,1621,1623,1625,1628],{"class":35,"line":62},[33,1601,175],{"class":122},[33,1603,785],{"class":126},[33,1605,844],{"class":153},[33,1607,562],{"class":126},[33,1609,1610],{"class":166},"user",[33,1612,157],{"class":126},[33,1614,1615],{"class":166},"loading",[33,1617,1618],{"class":126}," } ",[33,1620,192],{"class":122},[33,1622,656],{"class":146},[33,1624,150],{"class":126},[33,1626,1627],{"class":133},"'\u002Fapi\u002Fme'",[33,1629,347],{"class":126},[33,1631,1632,1634,1637,1639,1641],{"class":35,"line":68},[33,1633,175],{"class":122},[33,1635,1636],{"class":166}," prevUser",[33,1638,163],{"class":122},[33,1640,1395],{"class":146},[33,1642,1643],{"class":126},"(user)\n",[33,1645,1646,1648],{"class":35,"line":74},[33,1647,287],{"class":122},[33,1649,1650],{"class":126}," { user, prevUser, loading }\n",[33,1652,1653],{"class":35,"line":80},[33,1654,296],{"class":126},[33,1656,1657],{"class":35,"line":85},[33,1658,59],{"emptyLinePlaceholder":58},[33,1660,1661,1663,1666],{"class":35,"line":91},[33,1662,143],{"class":122},[33,1664,1665],{"class":146}," useAuth",[33,1667,318],{"class":126},[33,1669,1670,1672,1674,1676,1678,1680,1682,1684,1686],{"class":35,"line":97},[33,1671,175],{"class":122},[33,1673,785],{"class":126},[33,1675,1610],{"class":166},[33,1677,157],{"class":126},[33,1679,1615],{"class":166},[33,1681,1618],{"class":126},[33,1683,192],{"class":122},[33,1685,1595],{"class":146},[33,1687,739],{"class":126},[33,1689,1690,1692,1694,1697,1699,1702,1704,1706,1708,1710,1713,1715,1717],{"class":35,"line":273},[33,1691,175],{"class":122},[33,1693,178],{"class":126},[33,1695,1696],{"class":166},"token",[33,1698,157],{"class":126},[33,1700,1701],{"class":166},"setToken",[33,1703,189],{"class":126},[33,1705,192],{"class":122},[33,1707,988],{"class":146},[33,1709,150],{"class":126},[33,1711,1712],{"class":133},"'authToken'",[33,1714,157],{"class":126},[33,1716,506],{"class":166},[33,1718,347],{"class":126},[33,1720,1721,1723,1726,1728,1731,1734,1736],{"class":35,"line":279},[33,1722,175],{"class":122},[33,1724,1725],{"class":166}," isAuthenticated",[33,1727,163],{"class":122},[33,1729,1730],{"class":146}," Boolean",[33,1732,1733],{"class":126},"(user ",[33,1735,1254],{"class":122},[33,1737,1738],{"class":126}," token)\n",[33,1740,1741],{"class":35,"line":284},[33,1742,59],{"emptyLinePlaceholder":58},[33,1744,1745,1747,1750,1752,1755,1757,1759],{"class":35,"line":293},[33,1746,175],{"class":122},[33,1748,1749],{"class":166}," logout",[33,1751,163],{"class":122},[33,1753,1754],{"class":146}," useCallback",[33,1756,210],{"class":126},[33,1758,213],{"class":122},[33,1760,216],{"class":126},[33,1762,1763,1766,1768,1770],{"class":35,"line":299},[33,1764,1765],{"class":146},"    setToken",[33,1767,150],{"class":126},[33,1769,506],{"class":166},[33,1771,347],{"class":126},[33,1773,1774,1776,1778,1781,1784,1787],{"class":35,"line":304},[33,1775,758],{"class":146},[33,1777,150],{"class":126},[33,1779,1780],{"class":133},"'\u002Fapi\u002Flogout'",[33,1782,1783],{"class":126},", { method: ",[33,1785,1786],{"class":133},"'POST'",[33,1788,753],{"class":126},[33,1790,1791],{"class":35,"line":310},[33,1792,1793],{"class":126},"  }, [setToken])\n",[33,1795,1796],{"class":35,"line":321},[33,1797,59],{"emptyLinePlaceholder":58},[33,1799,1800,1802],{"class":35,"line":350},[33,1801,287],{"class":122},[33,1803,1804],{"class":126}," { user, loading, isAuthenticated, token, setToken, logout }\n",[33,1806,1807],{"class":35,"line":370},[33,1808,296],{"class":126},[33,1810,1811],{"class":35,"line":376},[33,1812,59],{"emptyLinePlaceholder":58},[33,1814,1815],{"class":35,"line":381},[33,1816,1817],{"class":39},"\u002F\u002F This composition is what makes hooks powerful: complex logic built from\n",[33,1819,1820],{"class":35,"line":392},[33,1821,1822],{"class":39},"\u002F\u002F simple, testable, reusable pieces — without inheritance or render props.\n",[14,1824,1826],{"id":1825},"tips-tricks","💡 Tips & Tricks",[19,1828,1830],{"filename":1829,"language":22},"tips.js",[24,1831,1833],{"className":26,"code":1832,"language":22,"meta":28,"style":28},"\u002F\u002F [Idiom] Name hooks with \"use\" + what they return\u002Fmanage: useDebounce, useFetch,\n\u002F\u002F useLocalStorage, useToggle, useMediaQuery. The name should describe the capability.\n\n\u002F\u002F [Idiom] Return an object (not an array) when the hook has 3+ values — callers\n\u002F\u002F can destructure only what they need without worrying about position:\n\u002F\u002F   const { data, loading, error } = useFetch(url)  \u002F\u002F better than [data, loading, error]\n\u002F\u002F Return an array for 2-value hooks (value, setter) to match useState convention.\n\n\u002F\u002F [Debug] If a custom hook seems to cause stale values, check dependency arrays\n\u002F\u002F in every internal hook. A missing dep in a nested useEffect\u002FuseCallback\n\u002F\u002F captures stale values silently.\n\n\u002F\u002F [Idiom] Keep hooks focused on ONE responsibility. useAuth shouldn't also\n\u002F\u002F handle theming. Compose small hooks into larger ones rather than building\n\u002F\u002F one mega-hook.\n\n\u002F\u002F [Safety] Guard against SSR — localStorage, window, and document don't exist\n\u002F\u002F during server rendering. Wrap in try\u002Fcatch or check typeof window !== 'undefined'.\n",[30,1834,1835,1840,1845,1849,1854,1859,1867,1872,1876,1881,1886,1891,1895,1900,1905,1910,1914,1919],{"__ignoreMap":28},[33,1836,1837],{"class":35,"line":36},[33,1838,1839],{"class":39},"\u002F\u002F [Idiom] Name hooks with \"use\" + what they return\u002Fmanage: useDebounce, useFetch,\n",[33,1841,1842],{"class":35,"line":43},[33,1843,1844],{"class":39},"\u002F\u002F useLocalStorage, useToggle, useMediaQuery. The name should describe the capability.\n",[33,1846,1847],{"class":35,"line":49},[33,1848,59],{"emptyLinePlaceholder":58},[33,1850,1851],{"class":35,"line":55},[33,1852,1853],{"class":39},"\u002F\u002F [Idiom] Return an object (not an array) when the hook has 3+ values — callers\n",[33,1855,1856],{"class":35,"line":62},[33,1857,1858],{"class":39},"\u002F\u002F can destructure only what they need without worrying about position:\n",[33,1860,1861,1864],{"class":35,"line":68},[33,1862,1863],{"class":39},"\u002F\u002F   const { data, loading, error } = useFetch(url)",[33,1865,1866],{"class":39},"  \u002F\u002F better than [data, loading, error]\n",[33,1868,1869],{"class":35,"line":74},[33,1870,1871],{"class":39},"\u002F\u002F Return an array for 2-value hooks (value, setter) to match useState convention.\n",[33,1873,1874],{"class":35,"line":80},[33,1875,59],{"emptyLinePlaceholder":58},[33,1877,1878],{"class":35,"line":85},[33,1879,1880],{"class":39},"\u002F\u002F [Debug] If a custom hook seems to cause stale values, check dependency arrays\n",[33,1882,1883],{"class":35,"line":91},[33,1884,1885],{"class":39},"\u002F\u002F in every internal hook. A missing dep in a nested useEffect\u002FuseCallback\n",[33,1887,1888],{"class":35,"line":97},[33,1889,1890],{"class":39},"\u002F\u002F captures stale values silently.\n",[33,1892,1893],{"class":35,"line":273},[33,1894,59],{"emptyLinePlaceholder":58},[33,1896,1897],{"class":35,"line":279},[33,1898,1899],{"class":39},"\u002F\u002F [Idiom] Keep hooks focused on ONE responsibility. useAuth shouldn't also\n",[33,1901,1902],{"class":35,"line":284},[33,1903,1904],{"class":39},"\u002F\u002F handle theming. Compose small hooks into larger ones rather than building\n",[33,1906,1907],{"class":35,"line":293},[33,1908,1909],{"class":39},"\u002F\u002F one mega-hook.\n",[33,1911,1912],{"class":35,"line":299},[33,1913,59],{"emptyLinePlaceholder":58},[33,1915,1916],{"class":35,"line":304},[33,1917,1918],{"class":39},"\u002F\u002F [Safety] Guard against SSR — localStorage, window, and document don't exist\n",[33,1920,1921],{"class":35,"line":310},[33,1922,1923],{"class":39},"\u002F\u002F during server rendering. Wrap in try\u002Fcatch or check typeof window !== 'undefined'.\n",[14,1925,1927],{"id":1926},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[19,1929,1931],{"filename":1930,"language":22},"edge_cases.js",[24,1932,1934],{"className":26,"code":1933,"language":22,"meta":28,"style":28},"\u002F\u002F [Gotcha] Two components using the same custom hook do NOT share state.\n\u002F\u002F Each component gets its OWN instance of the hook's useState\u002FuseEffect.\n\u002F\u002F Hooks reuse LOGIC, not STATE. Sharing state requires Context (Ch 7) or\n\u002F\u002F lifting state up to a common parent.\n\n\u002F\u002F [Gotcha] The \"use\" prefix is enforced by the linter, not React itself.\n\u002F\u002F A function named \"getData\" that calls useState won't be caught by the\n\u002F\u002F linter — and calling it conditionally will silently break the Rules of Hooks.\n\n\u002F\u002F [Gotcha] Hooks that accept functions as arguments need those functions to be\n\u002F\u002F stable (useCallback'd) if they're in a dependency array, or the hook will\n\u002F\u002F re-run on every render. useFetch(url, fetchFn) → if fetchFn isn't memoized,\n\u002F\u002F the effect re-runs every render → infinite fetch loop.\n\n\u002F\u002F [Gotcha] Returning a function from a custom hook that closes over state can\n\u002F\u002F create stale closures. Wrap returned functions in useCallback with proper deps.\n\n\u002F\u002F [Gotcha] SSR: localStorage, window, and document are undefined during server\n\u002F\u002F rendering. Accessing them in useState's initializer or useLayoutEffect throws.\n\u002F\u002F Guard with typeof window !== 'undefined' or use useEffect (which doesn't run on server).\n",[30,1935,1936,1941,1946,1951,1956,1960,1965,1970,1975,1979,1984,1989,1994,1999,2003,2008,2013,2017,2022,2027],{"__ignoreMap":28},[33,1937,1938],{"class":35,"line":36},[33,1939,1940],{"class":39},"\u002F\u002F [Gotcha] Two components using the same custom hook do NOT share state.\n",[33,1942,1943],{"class":35,"line":43},[33,1944,1945],{"class":39},"\u002F\u002F Each component gets its OWN instance of the hook's useState\u002FuseEffect.\n",[33,1947,1948],{"class":35,"line":49},[33,1949,1950],{"class":39},"\u002F\u002F Hooks reuse LOGIC, not STATE. Sharing state requires Context (Ch 7) or\n",[33,1952,1953],{"class":35,"line":55},[33,1954,1955],{"class":39},"\u002F\u002F lifting state up to a common parent.\n",[33,1957,1958],{"class":35,"line":62},[33,1959,59],{"emptyLinePlaceholder":58},[33,1961,1962],{"class":35,"line":68},[33,1963,1964],{"class":39},"\u002F\u002F [Gotcha] The \"use\" prefix is enforced by the linter, not React itself.\n",[33,1966,1967],{"class":35,"line":74},[33,1968,1969],{"class":39},"\u002F\u002F A function named \"getData\" that calls useState won't be caught by the\n",[33,1971,1972],{"class":35,"line":80},[33,1973,1974],{"class":39},"\u002F\u002F linter — and calling it conditionally will silently break the Rules of Hooks.\n",[33,1976,1977],{"class":35,"line":85},[33,1978,59],{"emptyLinePlaceholder":58},[33,1980,1981],{"class":35,"line":91},[33,1982,1983],{"class":39},"\u002F\u002F [Gotcha] Hooks that accept functions as arguments need those functions to be\n",[33,1985,1986],{"class":35,"line":97},[33,1987,1988],{"class":39},"\u002F\u002F stable (useCallback'd) if they're in a dependency array, or the hook will\n",[33,1990,1991],{"class":35,"line":273},[33,1992,1993],{"class":39},"\u002F\u002F re-run on every render. useFetch(url, fetchFn) → if fetchFn isn't memoized,\n",[33,1995,1996],{"class":35,"line":279},[33,1997,1998],{"class":39},"\u002F\u002F the effect re-runs every render → infinite fetch loop.\n",[33,2000,2001],{"class":35,"line":284},[33,2002,59],{"emptyLinePlaceholder":58},[33,2004,2005],{"class":35,"line":293},[33,2006,2007],{"class":39},"\u002F\u002F [Gotcha] Returning a function from a custom hook that closes over state can\n",[33,2009,2010],{"class":35,"line":299},[33,2011,2012],{"class":39},"\u002F\u002F create stale closures. Wrap returned functions in useCallback with proper deps.\n",[33,2014,2015],{"class":35,"line":304},[33,2016,59],{"emptyLinePlaceholder":58},[33,2018,2019],{"class":35,"line":310},[33,2020,2021],{"class":39},"\u002F\u002F [Gotcha] SSR: localStorage, window, and document are undefined during server\n",[33,2023,2024],{"class":35,"line":321},[33,2025,2026],{"class":39},"\u002F\u002F rendering. Accessing them in useState's initializer or useLayoutEffect throws.\n",[33,2028,2029],{"class":35,"line":350},[33,2030,2031],{"class":39},"\u002F\u002F Guard with typeof window !== 'undefined' or use useEffect (which doesn't run on server).\n",[14,2033,2035],{"id":2034},"spot-the-bug","🧠 Spot the Bug",[2037,2038,2039,2040,2043],"p",{},"A ",[30,2041,2042],{},"useToggle"," hook works on the first click but stops toggling after that:",[19,2045,2047],{"filename":2046,"language":22},"spot_the_bug.js",[24,2048,2050],{"className":26,"code":2049,"language":22,"meta":28,"style":28},"function useToggle(initialValue = false) {\n  const [value, setValue] = useState(initialValue)\n  const toggle = () => setValue(!value)\n  return [value, toggle]\n}\n",[30,2051,2052,2070,2091,2114,2121],{"__ignoreMap":28},[33,2053,2054,2056,2059,2061,2063,2065,2068],{"class":35,"line":36},[33,2055,143],{"class":122},[33,2057,2058],{"class":146}," useToggle",[33,2060,150],{"class":126},[33,2062,998],{"class":153},[33,2064,163],{"class":122},[33,2066,2067],{"class":166}," false",[33,2069,170],{"class":126},[33,2071,2072,2074,2076,2078,2080,2082,2084,2086,2088],{"class":35,"line":43},[33,2073,175],{"class":122},[33,2075,178],{"class":126},[33,2077,154],{"class":166},[33,2079,157],{"class":126},[33,2081,1018],{"class":166},[33,2083,189],{"class":126},[33,2085,192],{"class":122},[33,2087,195],{"class":146},[33,2089,2090],{"class":126},"(initialValue)\n",[33,2092,2093,2095,2098,2100,2102,2104,2107,2109,2111],{"class":35,"line":49},[33,2094,175],{"class":122},[33,2096,2097],{"class":146}," toggle",[33,2099,163],{"class":122},[33,2101,247],{"class":126},[33,2103,213],{"class":122},[33,2105,2106],{"class":146}," setValue",[33,2108,150],{"class":126},[33,2110,715],{"class":122},[33,2112,2113],{"class":126},"value)\n",[33,2115,2116,2118],{"class":35,"line":55},[33,2117,287],{"class":122},[33,2119,2120],{"class":126}," [value, toggle]\n",[33,2122,2123],{"class":35,"line":62},[33,2124,296],{"class":126},[2126,2127,2128,2132,2162,2168,2251],"details",{},[2129,2130,2131],"summary",{},"Answer",[2037,2133,2134,2137,2138,2140,2141,2145,2146,2148,2149,2151,2152,2154,2155,2158,2159,2161],{},[30,2135,2136],{},"toggle"," captures ",[30,2139,154],{}," from the ",[2142,2143,2144],"strong",{},"current render's closure",". After the first toggle, ",[30,2147,154],{}," changes and the component re-renders — but if ",[30,2150,2136],{}," was captured by a consumer (e.g., passed to a memoized child or stored in a variable), the old closure with the old ",[30,2153,154],{}," persists. On the next call, ",[30,2156,2157],{},"setValue(!value)"," uses the stale ",[30,2160,154],{}," — toggling back to the original state, then back again, appearing to \"stop toggling.\"",[2037,2163,2164,2167],{},[2142,2165,2166],{},"Fix",": use the functional updater form so it doesn't depend on the closure:",[24,2169,2171],{"className":26,"code":2170,"language":22,"meta":28,"style":28},"function useToggle(initialValue = false) {\n  const [value, setValue] = useState(initialValue)\n  const toggle = useCallback(() => setValue(prev => !prev), [])  \u002F\u002F stable + no stale closure\n  return [value, toggle]\n}\n",[30,2172,2173,2189,2209,2241,2247],{"__ignoreMap":28},[33,2174,2175,2177,2179,2181,2183,2185,2187],{"class":35,"line":36},[33,2176,143],{"class":122},[33,2178,2058],{"class":146},[33,2180,150],{"class":126},[33,2182,998],{"class":153},[33,2184,163],{"class":122},[33,2186,2067],{"class":166},[33,2188,170],{"class":126},[33,2190,2191,2193,2195,2197,2199,2201,2203,2205,2207],{"class":35,"line":43},[33,2192,175],{"class":122},[33,2194,178],{"class":126},[33,2196,154],{"class":166},[33,2198,157],{"class":126},[33,2200,1018],{"class":166},[33,2202,189],{"class":126},[33,2204,192],{"class":122},[33,2206,195],{"class":146},[33,2208,2090],{"class":126},[33,2210,2211,2213,2215,2217,2219,2221,2223,2225,2227,2230,2232,2235,2238],{"class":35,"line":49},[33,2212,175],{"class":122},[33,2214,2097],{"class":166},[33,2216,163],{"class":122},[33,2218,1754],{"class":146},[33,2220,210],{"class":126},[33,2222,213],{"class":122},[33,2224,2106],{"class":146},[33,2226,150],{"class":126},[33,2228,2229],{"class":153},"prev",[33,2231,452],{"class":122},[33,2233,2234],{"class":122}," !",[33,2236,2237],{"class":126},"prev), [])  ",[33,2239,2240],{"class":39},"\u002F\u002F stable + no stale closure\n",[33,2242,2243,2245],{"class":35,"line":55},[33,2244,287],{"class":122},[33,2246,2120],{"class":126},[33,2248,2249],{"class":35,"line":62},[33,2250,296],{"class":126},[2037,2252,2253,2254,2257,2258,2261,2262,2264,2265,2268],{},"The ",[30,2255,2256],{},"useCallback"," with ",[30,2259,2260],{},"[]"," makes ",[30,2263,2136],{}," a stable reference (no re-creation), and ",[30,2266,2267],{},"prev => !prev"," uses the functional updater so it always has the latest value — no stale closure.",[14,2270,2272],{"id":2271},"key-takeaways","Key Takeaways",[19,2274,2276],{"filename":2275,"language":22},"key_takeaways.js",[24,2277,2279],{"className":26,"code":2278,"language":22,"meta":28,"style":28},"\u002F\u002F 1. Custom hooks = reusable stateful LOGIC (not state itself). Each component\n\u002F\u002F    using a hook gets its OWN independent state instance. Share STATE via Context.\n\n\u002F\u002F 2. The \"use\" prefix is a linter signal, not just convention. Without it, the\n\u002F\u002F    Rules of Hooks aren't checked — conditional hook calls go undetected.\n\n\u002F\u002F 3. Common production hooks: useDebounce (debounce values), useFetch (data\n\u002F\u002F    fetching + cancellation), useLocalStorage (persist + cross-tab sync),\n\u002F\u002F    usePrevious (access prior render's value).\n\n\u002F\u002F 4. Compose hooks from other hooks: useAuth → useUser + useLocalStorage.\n\u002F\u002F    Build complex logic from small, focused, testable pieces.\n\n\u002F\u002F 5. Functions returned from hooks should be wrapped in useCallback with proper\n\u002F\u002F    deps to prevent stale closures and ensure stable references for consumers.\n\u002F\u002F    Guard against SSR: typeof window !== 'undefined' for browser APIs.\n",[30,2280,2281,2286,2291,2295,2300,2305,2309,2314,2319,2324,2328,2333,2338,2342,2347,2352],{"__ignoreMap":28},[33,2282,2283],{"class":35,"line":36},[33,2284,2285],{"class":39},"\u002F\u002F 1. Custom hooks = reusable stateful LOGIC (not state itself). Each component\n",[33,2287,2288],{"class":35,"line":43},[33,2289,2290],{"class":39},"\u002F\u002F    using a hook gets its OWN independent state instance. Share STATE via Context.\n",[33,2292,2293],{"class":35,"line":49},[33,2294,59],{"emptyLinePlaceholder":58},[33,2296,2297],{"class":35,"line":55},[33,2298,2299],{"class":39},"\u002F\u002F 2. The \"use\" prefix is a linter signal, not just convention. Without it, the\n",[33,2301,2302],{"class":35,"line":62},[33,2303,2304],{"class":39},"\u002F\u002F    Rules of Hooks aren't checked — conditional hook calls go undetected.\n",[33,2306,2307],{"class":35,"line":68},[33,2308,59],{"emptyLinePlaceholder":58},[33,2310,2311],{"class":35,"line":74},[33,2312,2313],{"class":39},"\u002F\u002F 3. Common production hooks: useDebounce (debounce values), useFetch (data\n",[33,2315,2316],{"class":35,"line":80},[33,2317,2318],{"class":39},"\u002F\u002F    fetching + cancellation), useLocalStorage (persist + cross-tab sync),\n",[33,2320,2321],{"class":35,"line":85},[33,2322,2323],{"class":39},"\u002F\u002F    usePrevious (access prior render's value).\n",[33,2325,2326],{"class":35,"line":91},[33,2327,59],{"emptyLinePlaceholder":58},[33,2329,2330],{"class":35,"line":97},[33,2331,2332],{"class":39},"\u002F\u002F 4. Compose hooks from other hooks: useAuth → useUser + useLocalStorage.\n",[33,2334,2335],{"class":35,"line":273},[33,2336,2337],{"class":39},"\u002F\u002F    Build complex logic from small, focused, testable pieces.\n",[33,2339,2340],{"class":35,"line":279},[33,2341,59],{"emptyLinePlaceholder":58},[33,2343,2344],{"class":35,"line":284},[33,2345,2346],{"class":39},"\u002F\u002F 5. Functions returned from hooks should be wrapped in useCallback with proper\n",[33,2348,2349],{"class":35,"line":293},[33,2350,2351],{"class":39},"\u002F\u002F    deps to prevent stale closures and ensure stable references for consumers.\n",[33,2353,2354],{"class":35,"line":299},[33,2355,2356],{"class":39},"\u002F\u002F    Guard against SSR: typeof window !== 'undefined' for browser APIs.\n",[2358,2359,2360],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":28,"searchDepth":43,"depth":43,"links":2362},[2363,2364,2370,2371,2372,2373,2374],{"id":16,"depth":43,"text":17},{"id":103,"depth":43,"text":104,"children":2365},[2366,2367,2368,2369],{"id":108,"depth":49,"text":109},{"id":466,"depth":49,"text":467},{"id":957,"depth":49,"text":958},{"id":1364,"depth":49,"text":1365},{"id":1565,"depth":43,"text":1566},{"id":1825,"depth":43,"text":1826},{"id":1926,"depth":43,"text":1927},{"id":2034,"depth":43,"text":2035},{"id":2271,"depth":43,"text":2272},"Extracting reusable stateful logic into custom hooks, naming conventions, composition patterns, rules of hooks, and production hooks: useDebounce, useFetch, useLocalStorage, usePrevious. Code-first reference for mid-to-senior React engineers.","md",{},"\u002Freact\u002F11-custom-hooks",{"title":5,"description":2375},"react\u002F11-custom-hooks","5uMPhvwgd8dZxCNQtaoxAuCxyp174Has0lN-QXWxxXE",1789924652186]