[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"knowledge-\u002Fapi\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Feffect":3,"knowledge-related-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Feffect":918,"knowledge-series-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Feffect":941},{"source":4,"connector":5,"page":6},"sqlite","better-sqlite3",{"path":7,"title":8,"description":9,"category":10,"tags":11,"date":15,"pinned":16,"draft":16,"body":17,"seo":914,"stem":915,"id":916,"extension":917},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Feffect","Effect 与副作用","useEffect 用于在组件渲染后执行与外部系统同步的逻辑（请求、订阅、手动改 DOM 等）。涵盖基本用法、依赖数组、适用\u002F不适用场景、useLayoutEffect、最佳实践与常见坑。","技能小册",[12,13,14],"react","hooks","effect","2024-10-11",false,{"type":18,"value":19,"toc":898},"minimark",[20,24,43,47,60,63,193,239,242,297,308,313,379,382,448,461,465,471,477,499,503,596,599,603,616,678,681,711,714,803,806,826,829,832,894],[21,22,8],"h1",{"id":23},"effect-与副作用",[25,26,27,31,32,36,37,42],"p",{},[28,29,30],"code",{},"useEffect"," 用于在组件渲染后执行",[33,34,35],"strong",{},"与外部系统同步","的逻辑（请求、订阅、手动改 DOM 等）。见 ",[38,39,41],"a",{"href":40},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Fstate","State","。",[44,45,46],"h2",{"id":46},"核心概念",[25,48,49,51,52,55,56,59],{},[28,50,30],{}," 解决的问题是：「组件渲染出来之后，我需要让某个",[33,53,54],{},"外部系统","（DOM、网络、第三方库、浏览器 API）与当前的 props\u002Fstate 保持同步」。它不是「在某个时机运行一段代码」的万能钩子，而是专门为",[33,57,58],{},"同步","这件事设计的——理解这一点，才能判断什么时候该用 Effect，什么时候不该用。",[44,61,62],{"id":62},"基本用法",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-tsx shiki shiki-themes material-theme-lighter github-light github-dark","import { useEffect, useState } from \"react\";\n\nfunction UserProfile({ userId }: { userId: string }) {\n  const [user, setUser] = useState\u003CUser | null>(null);\n\n  useEffect(() => {\n    let cancelled = false;\n    fetch(`\u002Fapi\u002Fusers\u002F${userId}`)\n      .then((r) => r.json())\n      .then((data) => {\n        if (!cancelled) setUser(data);\n      });\n    return () => {\n      cancelled = true;\n    };\n  }, [userId]);\n\n  if (!user) return \u003Cp>加载中…\u003C\u002Fp>;\n  return \u003Cp>{user.name}\u003C\u002Fp>;\n}\n","tsx","",[28,71,72,80,87,93,99,104,110,116,122,128,134,140,146,152,158,164,170,175,181,187],{"__ignoreMap":69},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,79],{},"import { useEffect, useState } from \"react\";\n",[73,81,83],{"class":75,"line":82},2,[73,84,86],{"emptyLinePlaceholder":85},true,"\n",[73,88,90],{"class":75,"line":89},3,[73,91,92],{},"function UserProfile({ userId }: { userId: string }) {\n",[73,94,96],{"class":75,"line":95},4,[73,97,98],{},"  const [user, setUser] = useState\u003CUser | null>(null);\n",[73,100,102],{"class":75,"line":101},5,[73,103,86],{"emptyLinePlaceholder":85},[73,105,107],{"class":75,"line":106},6,[73,108,109],{},"  useEffect(() => {\n",[73,111,113],{"class":75,"line":112},7,[73,114,115],{},"    let cancelled = false;\n",[73,117,119],{"class":75,"line":118},8,[73,120,121],{},"    fetch(`\u002Fapi\u002Fusers\u002F${userId}`)\n",[73,123,125],{"class":75,"line":124},9,[73,126,127],{},"      .then((r) => r.json())\n",[73,129,131],{"class":75,"line":130},10,[73,132,133],{},"      .then((data) => {\n",[73,135,137],{"class":75,"line":136},11,[73,138,139],{},"        if (!cancelled) setUser(data);\n",[73,141,143],{"class":75,"line":142},12,[73,144,145],{},"      });\n",[73,147,149],{"class":75,"line":148},13,[73,150,151],{},"    return () => {\n",[73,153,155],{"class":75,"line":154},14,[73,156,157],{},"      cancelled = true;\n",[73,159,161],{"class":75,"line":160},15,[73,162,163],{},"    };\n",[73,165,167],{"class":75,"line":166},16,[73,168,169],{},"  }, [userId]);\n",[73,171,173],{"class":75,"line":172},17,[73,174,86],{"emptyLinePlaceholder":85},[73,176,178],{"class":75,"line":177},18,[73,179,180],{},"  if (!user) return \u003Cp>加载中…\u003C\u002Fp>;\n",[73,182,184],{"class":75,"line":183},19,[73,185,186],{},"  return \u003Cp>{user.name}\u003C\u002Fp>;\n",[73,188,190],{"class":75,"line":189},20,[73,191,192],{},"}\n",[194,195,196,209],"table",{},[197,198,199],"thead",{},[200,201,202,206],"tr",{},[203,204,205],"th",{},"部分",[203,207,208],{},"说明",[210,211,212,228],"tbody",{},[200,213,214,221],{},[215,216,217,218],"td",{},"依赖数组 ",[28,219,220],{},"[userId]",[215,222,223,224,227],{},"仅当 ",[28,225,226],{},"userId"," 变化时重新执行 effect",[200,229,230,236],{},[215,231,232,233],{},"清理函数 ",[28,234,235],{},"return () => {...}",[215,237,238],{},"下次 effect 执行前或组件卸载时调用，用于取消订阅、忽略过期请求",[44,240,241],{"id":241},"依赖数组",[194,243,244,254],{},[197,245,246],{},[200,247,248,251],{},[203,249,250],{},"写法",[203,252,253],{},"行为",[210,255,256,267,281],{},[200,257,258,261],{},[215,259,260],{},"省略第二参数",[215,262,263,266],{},[33,264,265],{},"每次渲染后","都执行（慎用）",[200,268,269,274],{},[215,270,271],{},[28,272,273],{},"[]",[215,275,276,277,280],{},"仅",[33,278,279],{},"挂载后","执行一次（注意 StrictMode 开发下可能执行两次）",[200,282,283,288],{},[215,284,285],{},[28,286,287],{},"[a, b]",[215,289,290,292,293,296],{},[28,291,38],{}," 或 ",[28,294,295],{},"b"," 变化时执行",[25,298,299,300,303,304,307],{},"依赖须包含 effect 内读取的",[33,301,302],{},"所有响应式值","（props、state、组件内变量），否则可能读到陈旧闭包。由 ESLint ",[28,305,306],{},"react-hooks\u002Fexhaustive-deps"," 辅助检查。",[309,310,312],"h3",{"id":311},"示例依赖遗漏导致的陈旧闭包","示例：依赖遗漏导致的陈旧闭包",[64,314,316],{"className":66,"code":315,"language":68,"meta":69,"style":69},"function SearchBox() {\n  const [query, setQuery] = useState(\"\");\n  const [debounced, setDebounced] = useState(\"\");\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setDebounced(query); \u002F\u002F 若依赖数组漏写 query，这里永远是首次渲染时的空字符串\n    }, 300);\n    return () => clearTimeout(timer);\n  }, [query]); \u002F\u002F 正确：包含 query\n\n  return \u003Cinput value={query} onChange={(e) => setQuery(e.target.value)} \u002F>;\n}\n",[28,317,318,323,328,333,337,341,346,351,356,361,366,370,375],{"__ignoreMap":69},[73,319,320],{"class":75,"line":76},[73,321,322],{},"function SearchBox() {\n",[73,324,325],{"class":75,"line":82},[73,326,327],{},"  const [query, setQuery] = useState(\"\");\n",[73,329,330],{"class":75,"line":89},[73,331,332],{},"  const [debounced, setDebounced] = useState(\"\");\n",[73,334,335],{"class":75,"line":95},[73,336,86],{"emptyLinePlaceholder":85},[73,338,339],{"class":75,"line":101},[73,340,109],{},[73,342,343],{"class":75,"line":106},[73,344,345],{},"    const timer = setTimeout(() => {\n",[73,347,348],{"class":75,"line":112},[73,349,350],{},"      setDebounced(query); \u002F\u002F 若依赖数组漏写 query，这里永远是首次渲染时的空字符串\n",[73,352,353],{"class":75,"line":118},[73,354,355],{},"    }, 300);\n",[73,357,358],{"class":75,"line":124},[73,359,360],{},"    return () => clearTimeout(timer);\n",[73,362,363],{"class":75,"line":130},[73,364,365],{},"  }, [query]); \u002F\u002F 正确：包含 query\n",[73,367,368],{"class":75,"line":136},[73,369,86],{"emptyLinePlaceholder":85},[73,371,372],{"class":75,"line":142},[73,373,374],{},"  return \u003Cinput value={query} onChange={(e) => setQuery(e.target.value)} \u002F>;\n",[73,376,377],{"class":75,"line":148},[73,378,192],{},[44,380,381],{"id":381},"订阅外部事件的完整示例",[64,383,385],{"className":66,"code":384,"language":68,"meta":69,"style":69},"function useWindowWidth() {\n  const [width, setWidth] = useState(() => window.innerWidth);\n\n  useEffect(() => {\n    function handleResize() {\n      setWidth(window.innerWidth);\n    }\n    window.addEventListener(\"resize\", handleResize);\n    return () => window.removeEventListener(\"resize\", handleResize); \u002F\u002F 必须清理，否则组件卸载后仍在监听\n  }, []);\n\n  return width;\n}\n",[28,386,387,392,397,401,405,410,415,420,425,430,435,439,444],{"__ignoreMap":69},[73,388,389],{"class":75,"line":76},[73,390,391],{},"function useWindowWidth() {\n",[73,393,394],{"class":75,"line":82},[73,395,396],{},"  const [width, setWidth] = useState(() => window.innerWidth);\n",[73,398,399],{"class":75,"line":89},[73,400,86],{"emptyLinePlaceholder":85},[73,402,403],{"class":75,"line":95},[73,404,109],{},[73,406,407],{"class":75,"line":101},[73,408,409],{},"    function handleResize() {\n",[73,411,412],{"class":75,"line":106},[73,413,414],{},"      setWidth(window.innerWidth);\n",[73,416,417],{"class":75,"line":112},[73,418,419],{},"    }\n",[73,421,422],{"class":75,"line":118},[73,423,424],{},"    window.addEventListener(\"resize\", handleResize);\n",[73,426,427],{"class":75,"line":124},[73,428,429],{},"    return () => window.removeEventListener(\"resize\", handleResize); \u002F\u002F 必须清理，否则组件卸载后仍在监听\n",[73,431,432],{"class":75,"line":130},[73,433,434],{},"  }, []);\n",[73,436,437],{"class":75,"line":136},[73,438,86],{"emptyLinePlaceholder":85},[73,440,441],{"class":75,"line":142},[73,442,443],{},"  return width;\n",[73,445,446],{"class":75,"line":148},[73,447,192],{},[25,449,450,453,454,457,458,42],{},[28,451,452],{},"resize"," 事件、",[28,455,456],{},"WebSocket"," 连接、第三方库实例（地图、图表）都遵循同一个模式：",[33,459,460],{},"在 Effect 中建立连接，在清理函数中断开",[44,462,464],{"id":463},"effect-适用场景","Effect 适用场景",[25,466,467,470],{},[33,468,469],{},"适合","：连接外部系统、订阅、非 React 控制的 DOM 操作。",[25,472,473,476],{},[33,474,475],{},"不适合","（优先考虑其他方式）：",[478,479,480,484,491],"ul",{},[481,482,483],"li",{},"由事件触发的业务逻辑 → 写在事件处理函数中。",[481,485,486,487,490],{},"派生数据 → 用 ",[28,488,489],{},"useMemo"," 或在渲染时直接计算。",[481,492,493,494,498],{},"服务端数据缓存 → 考虑 ",[38,495,497],{"href":496},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fecology","TanStack Query"," 等。",[309,500,502],{"id":501},"反例本该在事件处理函数里的逻辑","反例：本该在事件处理函数里的逻辑",[64,504,506],{"className":66,"code":505,"language":68,"meta":69,"style":69},"\u002F\u002F 反例：用 Effect 模拟「点击后跳转」，多了一次不必要的渲染，且逻辑意图不直观\nfunction Form() {\n  const [submitted, setSubmitted] = useState(false);\n  useEffect(() => {\n    if (submitted) {\n      navigate(\"\u002Fsuccess\");\n    }\n  }, [submitted]);\n\n  return \u003Cbutton onClick={() => setSubmitted(true)}>提交\u003C\u002Fbutton>;\n}\n\n\u002F\u002F 正确：直接在事件处理函数中处理\nfunction Form() {\n  function handleSubmit() {\n    navigate(\"\u002Fsuccess\");\n  }\n  return \u003Cbutton onClick={handleSubmit}>提交\u003C\u002Fbutton>;\n}\n",[28,507,508,513,518,523,527,532,537,541,546,550,555,559,563,568,572,577,582,587,592],{"__ignoreMap":69},[73,509,510],{"class":75,"line":76},[73,511,512],{},"\u002F\u002F 反例：用 Effect 模拟「点击后跳转」，多了一次不必要的渲染，且逻辑意图不直观\n",[73,514,515],{"class":75,"line":82},[73,516,517],{},"function Form() {\n",[73,519,520],{"class":75,"line":89},[73,521,522],{},"  const [submitted, setSubmitted] = useState(false);\n",[73,524,525],{"class":75,"line":95},[73,526,109],{},[73,528,529],{"class":75,"line":101},[73,530,531],{},"    if (submitted) {\n",[73,533,534],{"class":75,"line":106},[73,535,536],{},"      navigate(\"\u002Fsuccess\");\n",[73,538,539],{"class":75,"line":112},[73,540,419],{},[73,542,543],{"class":75,"line":118},[73,544,545],{},"  }, [submitted]);\n",[73,547,548],{"class":75,"line":124},[73,549,86],{"emptyLinePlaceholder":85},[73,551,552],{"class":75,"line":130},[73,553,554],{},"  return \u003Cbutton onClick={() => setSubmitted(true)}>提交\u003C\u002Fbutton>;\n",[73,556,557],{"class":75,"line":136},[73,558,192],{},[73,560,561],{"class":75,"line":142},[73,562,86],{"emptyLinePlaceholder":85},[73,564,565],{"class":75,"line":148},[73,566,567],{},"\u002F\u002F 正确：直接在事件处理函数中处理\n",[73,569,570],{"class":75,"line":154},[73,571,517],{},[73,573,574],{"class":75,"line":160},[73,575,576],{},"  function handleSubmit() {\n",[73,578,579],{"class":75,"line":166},[73,580,581],{},"    navigate(\"\u002Fsuccess\");\n",[73,583,584],{"class":75,"line":172},[73,585,586],{},"  }\n",[73,588,589],{"class":75,"line":177},[73,590,591],{},"  return \u003Cbutton onClick={handleSubmit}>提交\u003C\u002Fbutton>;\n",[73,593,594],{"class":75,"line":183},[73,595,192],{},[25,597,598],{},"「由用户操作直接触发的后续动作」应该写在事件处理函数里，而不是通过一个 state 变量间接触发 Effect——后者多一次渲染，且难以看出因果关系。",[44,600,602],{"id":601},"uselayouteffect","useLayoutEffect",[25,604,605,606,609,610,612,613,615],{},"在浏览器",[33,607,608],{},"绘制前","同步执行，用于读取布局并同步更新 DOM，避免闪烁。一般优先 ",[28,611,30],{},"，仅在测量 DOM 后需立即改样式时使用 ",[28,614,602],{},"：",[64,617,619],{"className":66,"code":618,"language":68,"meta":69,"style":69},"function Tooltip({ targetRef }: { targetRef: RefObject\u003CHTMLElement> }) {\n  const [position, setPosition] = useState({ top: 0, left: 0 });\n  const tooltipRef = useRef\u003CHTMLDivElement>(null);\n\n  useLayoutEffect(() => {\n    const rect = targetRef.current!.getBoundingClientRect();\n    \u002F\u002F 在浏览器绘制前完成测量与定位，避免用户看到 tooltip 先出现在错误位置再跳到正确位置\n    setPosition({ top: rect.bottom, left: rect.left });\n  }, [targetRef]);\n\n  return \u003Cdiv ref={tooltipRef} style={{ position: \"absolute\", ...position }}>提示内容\u003C\u002Fdiv>;\n}\n",[28,620,621,626,631,636,640,645,650,655,660,665,669,674],{"__ignoreMap":69},[73,622,623],{"class":75,"line":76},[73,624,625],{},"function Tooltip({ targetRef }: { targetRef: RefObject\u003CHTMLElement> }) {\n",[73,627,628],{"class":75,"line":82},[73,629,630],{},"  const [position, setPosition] = useState({ top: 0, left: 0 });\n",[73,632,633],{"class":75,"line":89},[73,634,635],{},"  const tooltipRef = useRef\u003CHTMLDivElement>(null);\n",[73,637,638],{"class":75,"line":95},[73,639,86],{"emptyLinePlaceholder":85},[73,641,642],{"class":75,"line":101},[73,643,644],{},"  useLayoutEffect(() => {\n",[73,646,647],{"class":75,"line":106},[73,648,649],{},"    const rect = targetRef.current!.getBoundingClientRect();\n",[73,651,652],{"class":75,"line":112},[73,653,654],{},"    \u002F\u002F 在浏览器绘制前完成测量与定位，避免用户看到 tooltip 先出现在错误位置再跳到正确位置\n",[73,656,657],{"class":75,"line":118},[73,658,659],{},"    setPosition({ top: rect.bottom, left: rect.left });\n",[73,661,662],{"class":75,"line":124},[73,663,664],{},"  }, [targetRef]);\n",[73,666,667],{"class":75,"line":130},[73,668,86],{"emptyLinePlaceholder":85},[73,670,671],{"class":75,"line":136},[73,672,673],{},"  return \u003Cdiv ref={tooltipRef} style={{ position: \"absolute\", ...position }}>提示内容\u003C\u002Fdiv>;\n",[73,675,676],{"class":75,"line":142},[73,677,192],{},[44,679,680],{"id":680},"最佳实践",[478,682,683,686,692,698,701],{},[481,684,685],{},"写 Effect 前先问：「这是不是在同步外部系统？」如果答案是「不是」，大概率不该用 Effect。",[481,687,688,689,691],{},"依赖数组交给 ESLint ",[28,690,306],{}," 检查，不要手动省略依赖来\"控制\"执行时机——应该通过重构代码结构来控制。",[481,693,694,695,697],{},"网络请求类 Effect 一律处理竞态（取消\u002F忽略过期响应），否则快速切换 ",[28,696,226],{}," 等参数时可能显示错误的数据。",[481,699,700],{},"订阅\u002F连接类 Effect 必须返回清理函数，对称地建立与销毁。",[481,702,703,704,706,707,710],{},"复杂的数据获取逻辑迁移到专门的库（TanStack Query 等），而非手写一堂 ",[28,705,30],{}," + ",[28,708,709],{},"useState"," 处理 loading\u002Ferror\u002Fdata 三态。",[44,712,713],{"id":713},"常见坑",[194,715,716,729],{},[197,717,718],{},[200,719,720,723,726],{},[203,721,722],{},"现象",[203,724,725],{},"常见原因",[203,727,728],{},"处理",[210,730,731,750,761,778,789],{},[200,732,733,736,739],{},[215,734,735],{},"切换参数后短暂显示旧\u002F错误数据",[215,737,738],{},"未处理请求竞态（旧请求晚于新请求返回）",[215,740,741,742,745,746,749],{},"用 ",[28,743,744],{},"cancelled"," 标志位或 ",[28,747,748],{},"AbortController"," 忽略过期结果",[200,751,752,755,758],{},[215,753,754],{},"组件卸载后报「在已卸载组件上更新状态」警告",[215,756,757],{},"未清理订阅\u002F定时器\u002F请求回调",[215,759,760],{},"清理函数中取消订阅，或用取消标志位",[200,762,763,766,769],{},[215,764,765],{},"Effect 无限循环执行",[215,767,768],{},"依赖数组包含每次渲染都新建的对象\u002F函数",[215,770,741,771,773,774,777],{},[28,772,489],{},"\u002F",[28,775,776],{},"useCallback"," 稳定依赖，或调整依赖粒度",[200,779,780,783,786],{},[215,781,782],{},"ESLint 提示缺依赖但加上后逻辑「变了」",[215,784,785],{},"加上依赖后 Effect 执行时机变化，暴露了原有设计问题",[215,787,788],{},"重构代码而非禁用 lint 规则",[200,790,791,794,800],{},[215,792,793],{},"开发环境下 Effect 执行了两次",[215,795,796,799],{},[28,797,798],{},"StrictMode"," 故意双重调用以检测副作用问题",[215,801,802],{},"确保 Effect 具备正确的清理逻辑，而不是抑制该行为",[44,804,805],{"id":805},"延伸阅读",[478,807,808,813,819],{},[481,809,810,812],{},[38,811,41],{"href":40},"：与 Effect 配合的状态管理",[481,814,815,818],{},[38,816,817],{"href":496},"生态","：TanStack Query 替代手写数据请求 Effect",[481,820,821,825],{},[38,822,824],{"href":823},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Frendering","渲染与协调","：Effect 在 Commit 阶段之后执行",[44,827,828],{"id":828},"参考文献",[25,830,831],{},"以下链接在编写时均可正常访问：",[194,833,834,843],{},[197,835,836],{},[200,837,838,841],{},[203,839,840],{},"资料",[203,842,208],{},[210,844,845,858,870,882],{},[200,846,847,855],{},[215,848,849],{},[38,850,854],{"href":851,"rel":852},"https:\u002F\u002Fzh-hans.react.dev\u002Freference\u002Freact\u002FuseEffect",[853],"nofollow","React：useEffect",[215,856,857],{},"API",[200,859,860,867],{},[215,861,862],{},[38,863,866],{"href":864,"rel":865},"https:\u002F\u002Fzh-hans.react.dev\u002Flearn\u002Fsynchronizing-with-effects",[853],"React：同步 Effect",[215,868,869],{},"使用指南",[200,871,872,879],{},[215,873,874],{},[38,875,878],{"href":876,"rel":877},"https:\u002F\u002Fzh-hans.react.dev\u002Freference\u002Freact\u002FuseLayoutEffect",[853],"React：useLayoutEffect",[215,880,881],{},"布局 effect",[200,883,884,891],{},[215,885,886],{},[38,887,890],{"href":888,"rel":889},"https:\u002F\u002Fzh-hans.react.dev\u002Flearn\u002Fyou-might-not-need-an-effect",[853],"React：你可能不需要 Effect",[215,892,893],{},"反模式清单",[895,896,897],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":82,"depth":82,"links":899},[900,901,902,905,906,909,910,911,912,913],{"id":46,"depth":82,"text":46},{"id":62,"depth":82,"text":62},{"id":241,"depth":82,"text":241,"children":903},[904],{"id":311,"depth":89,"text":312},{"id":381,"depth":82,"text":381},{"id":463,"depth":82,"text":464,"children":907},[908],{"id":501,"depth":89,"text":502},{"id":601,"depth":82,"text":602},{"id":680,"depth":82,"text":680},{"id":713,"depth":82,"text":713},{"id":805,"depth":82,"text":805},{"id":828,"depth":82,"text":828},{"title":8,"description":9},"knowledge\u002Fbooklet\u002Fframe\u002FReact\u002Fhooks\u002Feffect","knowledge\u002Fknowledge\u002Fbooklet\u002Fframe\u002FReact\u002Fhooks\u002Feffect.md","md",{"items":919},[920,924,928,931,933,937],{"path":921,"title":922,"description":923,"category":10,"score":124},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Fcontext","Context","Context 在组件树中跨层级传递数据，避免 props 逐层透传。涵盖创建与使用、自定义 Hook 封装、性能拆分、与全局状态库对照、最佳实践与常见坑。",{"path":925,"title":926,"description":927,"category":10,"score":124},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fhooks\u002Fmemo","Memo、Callback 与 Ref","本文介绍 useRef、useMemo、useCallback、memo 及自定义 Hook。见 Effect。",{"path":40,"title":929,"description":930,"category":10,"score":124},"State 与 Reducer","Hooks 让函数组件拥有 state 与副作用能力。本文介绍 useState 与 useReducer。见 React 基础。",{"path":823,"title":824,"description":932,"category":10,"score":95},"React 将组件渲染为虚拟 DOM 树，再协调（reconciliation）到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。涵盖渲染流程、Fiber、startTransition、Suspense、最佳实践与常见坑。",{"path":934,"title":935,"description":936,"category":10,"score":95},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Fproject","工程化","新建 React 项目时，官方与社区主流工具为 Vite；全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。",{"path":938,"title":939,"description":940,"category":10,"score":95},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Freact\u002Ftypescript","TypeScript","React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。",{"series":942,"title":13,"items":943,"index":76,"prev":951,"next":952},"booklet\u002Fframe\u002Freact\u002Fhooks",[944,946,947,949],{"path":921,"title":922,"date":945,"pinned":16},"2021-10-16",{"path":7,"title":8,"date":15,"pinned":16},{"path":925,"title":926,"date":948,"pinned":16},"2021-02-28",{"path":40,"title":929,"date":950,"pinned":16},"2021-02-21",{"path":921,"title":922,"date":945,"pinned":16},{"path":925,"title":926,"date":948,"pinned":16}]