[{"data":1,"prerenderedAt":465},["ShallowReactive",2],{"article-react-controlled-form":3,"related-articles":364},{"id":4,"title":5,"body":6,"categoryLabel":351,"categorySlug":352,"description":353,"extension":354,"featured":63,"meta":355,"navigation":63,"path":356,"publishedAt":357,"readingMinutes":125,"seo":358,"stem":359,"tags":360,"__hash__":363},"blog\u002Fblog\u002Freact-controlled-form.md","React 受控表单：从一个输入框开始",{"type":7,"value":8,"toc":345},"minimark",[9,13,17,29,229,232,239,303,306,309,335,338,341],[10,11,12],"p",{},"表单是很常见的页面交互。React 里的“受控组件”听起来有些抽象，先从一个输入框开始：让输入框显示的值始终来自组件状态。",[14,15,16],"h2",{"id":16},"让状态成为数据来源",[10,18,19,20,24,25,28],{},"在受控输入框里，",[21,22,23],"code",{},"value"," 由 state 提供，每次输入时再通过 ",[21,26,27],{},"onChange"," 更新 state。这样页面显示的内容和 React 保存的值保持一致。",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-tsx shiki shiki-themes github-light-high-contrast","import { useState } from 'react'\n\nexport function NameField() {\n  const [name, setName] = useState('')\n\n  return (\n    \u003Clabel>\n      昵称\n      \u003Cinput\n        value={name}\n        onChange={(event) => setName(event.target.value)}\n      \u002F>\n    \u003C\u002Flabel>\n  )\n}\n","tsx","",[21,37,38,58,65,81,118,123,132,145,151,160,174,202,208,218,224],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sTJeM","import",[39,48,50],{"class":49},"sigWx"," { useState } ",[39,52,53],{"class":45},"from",[39,55,57],{"class":56},"sYEJz"," 'react'\n",[39,59,61],{"class":41,"line":60},2,[39,62,64],{"emptyLinePlaceholder":63},true,"\n",[39,66,68,71,74,78],{"class":41,"line":67},3,[39,69,70],{"class":45},"export",[39,72,73],{"class":45}," function",[39,75,77],{"class":76},"s3dhs"," NameField",[39,79,80],{"class":49},"() {\n",[39,82,84,87,90,94,97,100,103,106,109,112,115],{"class":41,"line":83},4,[39,85,86],{"class":45},"  const",[39,88,89],{"class":49}," [",[39,91,93],{"class":92},"sacAq","name",[39,95,96],{"class":49},", ",[39,98,99],{"class":92},"setName",[39,101,102],{"class":49},"] ",[39,104,105],{"class":45},"=",[39,107,108],{"class":76}," useState",[39,110,111],{"class":49},"(",[39,113,114],{"class":56},"''",[39,116,117],{"class":49},")\n",[39,119,121],{"class":41,"line":120},5,[39,122,64],{"emptyLinePlaceholder":63},[39,124,126,129],{"class":41,"line":125},6,[39,127,128],{"class":45},"  return",[39,130,131],{"class":49}," (\n",[39,133,135,138,142],{"class":41,"line":134},7,[39,136,137],{"class":49},"    \u003C",[39,139,141],{"class":140},"s742_","label",[39,143,144],{"class":49},">\n",[39,146,148],{"class":41,"line":147},8,[39,149,150],{"class":49},"      昵称\n",[39,152,154,157],{"class":41,"line":153},9,[39,155,156],{"class":49},"      \u003C",[39,158,159],{"class":140},"input\n",[39,161,163,166,169,171],{"class":41,"line":162},10,[39,164,165],{"class":92},"        value",[39,167,168],{"class":45},"={",[39,170,93],{"class":49},[39,172,173],{"class":45},"}\n",[39,175,177,180,182,184,188,191,194,197,200],{"class":41,"line":176},11,[39,178,179],{"class":92},"        onChange",[39,181,168],{"class":45},[39,183,111],{"class":49},[39,185,187],{"class":186},"sV4o_","event",[39,189,190],{"class":49},") ",[39,192,193],{"class":45},"=>",[39,195,196],{"class":76}," setName",[39,198,199],{"class":49},"(event.target.value)",[39,201,173],{"class":45},[39,203,205],{"class":41,"line":204},12,[39,206,207],{"class":49},"      \u002F>\n",[39,209,211,214,216],{"class":41,"line":210},13,[39,212,213],{"class":49},"    \u003C\u002F",[39,215,141],{"class":140},[39,217,144],{"class":49},[39,219,221],{"class":41,"line":220},14,[39,222,223],{"class":49},"  )\n",[39,225,227],{"class":41,"line":226},15,[39,228,173],{"class":49},[14,230,231],{"id":231},"处理表单提交",[10,233,234,235,238],{},"表单提交时可以读取当前 state。",[21,236,237],{},"preventDefault()"," 用来阻止浏览器默认刷新行为，之后再把数据交给页面自己的处理函数。",[30,240,242],{"className":32,"code":241,"language":34,"meta":35,"style":35},"function handleSubmit(event: React.FormEvent\u003CHTMLFormElement>) {\n  event.preventDefault()\n  console.log(name)\n}\n",[21,243,244,277,288,299],{"__ignoreMap":35},[39,245,246,249,252,254,256,259,262,265,268,271,274],{"class":41,"line":42},[39,247,248],{"class":45},"function",[39,250,251],{"class":76}," handleSubmit",[39,253,111],{"class":49},[39,255,187],{"class":186},[39,257,258],{"class":45},":",[39,260,261],{"class":186}," React",[39,263,264],{"class":49},".",[39,266,267],{"class":186},"FormEvent",[39,269,270],{"class":49},"\u003C",[39,272,273],{"class":186},"HTMLFormElement",[39,275,276],{"class":49},">) {\n",[39,278,279,282,285],{"class":41,"line":60},[39,280,281],{"class":49},"  event.",[39,283,284],{"class":76},"preventDefault",[39,286,287],{"class":49},"()\n",[39,289,290,293,296],{"class":41,"line":67},[39,291,292],{"class":49},"  console.",[39,294,295],{"class":76},"log",[39,297,298],{"class":49},"(name)\n",[39,300,301],{"class":41,"line":83},[39,302,173],{"class":49},[10,304,305],{},"真实项目里，提交前还需要做必填校验，并在提交期间给用户明确反馈。",[14,307,308],{"id":308},"容易遇到的小问题",[310,311,312,322,332],"ul",{},[313,314,315,316,318,319,321],"li",{},"给输入框设置了 ",[21,317,23],{},"，却没有 ",[21,320,27],{},"，输入框会变成只读。",[313,323,324,325,328,329,331],{},"初始值可能是 ",[21,326,327],{},"undefined","，之后又变成字符串。文本输入框最好从一开始就使用 ",[21,330,114],{},"。",[313,333,334],{},"表单里有多个字段时，可以分别管理状态；字段更多时再考虑统一的表单方案。",[14,336,337],{"id":337},"小结",[10,339,340],{},"受控输入框把数据流讲得很清楚：state 决定当前显示值，用户输入触发事件，事件再更新 state。理解这个循环后，表单校验和提交状态也更容易组织。",[342,343,344],"style",{},"html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sYEJz, html code.shiki .sYEJz{--shiki-default:#032563}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .s742_, html code.shiki .s742_{--shiki-default:#024C1A}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}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);}",{"title":35,"searchDepth":60,"depth":60,"links":346},[347,348,349,350],{"id":16,"depth":60,"text":16},{"id":231,"depth":60,"text":231},{"id":308,"depth":60,"text":308},{"id":337,"depth":60,"text":337},"React","react","用一个简单的姓名输入框，理解 value、onChange 与 React state 之间的关系。","md",{},"\u002Fblog\u002Freact-controlled-form","2026-09-18",{"title":5,"description":353},"blog\u002Freact-controlled-form",[351,361,362],"表单","学习笔记","sLM409XEN3kKxtzto8xEDN58Xb8TMbQQj5yLD2fSu1E",[365,377,387,389,397,405,413,421,429,439,447,455],{"id":366,"path":367,"title":368,"description":369,"categorySlug":370,"categoryLabel":371,"publishedAt":372,"readingMinutes":83,"featured":63,"tags":373},"blog\u002Fblog\u002Fcss-grid-cards.md","\u002Fblog\u002Fcss-grid-cards","用 CSS Grid 排列自适应卡片","使用 repeat、minmax 和 auto-fit，让卡片列数随容器宽度自然变化。","styles","页面与样式","2026-09-10",[374,375,376],"CSS","Grid","页面布局",{"id":378,"path":379,"title":380,"description":381,"categorySlug":370,"categoryLabel":371,"publishedAt":382,"readingMinutes":120,"featured":383,"tags":384},"blog\u002Fblog\u002Fmobile-layout-checklist.md","\u002Fblog\u002Fmobile-layout-checklist","做移动端页面适配时的检查笔记","从内容宽度、触控区域、文字换行和键盘焦点几个方面检查窄屏体验。","2026-08-20",false,[374,385,386],"响应式","可访问性",{"id":4,"path":356,"title":5,"description":353,"categorySlug":352,"categoryLabel":351,"publishedAt":357,"readingMinutes":125,"featured":63,"tags":388},[351,361,362],{"id":390,"path":391,"title":392,"description":393,"categorySlug":352,"categoryLabel":351,"publishedAt":394,"readingMinutes":83,"featured":383,"tags":395},"blog\u002Fblog\u002Freact-list-keys.md","\u002Fblog\u002Freact-list-keys","React 列表渲染里的 key 有什么用","通过稳定的 key 帮助 React 识别列表项目，避免用数组下标造成的状态错位。","2026-08-12",[351,396,362],"列表",{"id":398,"path":399,"title":400,"description":401,"categorySlug":352,"categoryLabel":351,"publishedAt":402,"readingMinutes":120,"featured":383,"tags":403},"blog\u002Fblog\u002Freact-props-and-state.md","\u002Fblog\u002Freact-props-and-state","React 组件里的 Props 和 State","用一个简单的信息卡片区分外部传入的数据和组件内部会变化的数据。","2026-08-28",[351,404,362],"组件",{"id":406,"path":407,"title":408,"description":409,"categorySlug":352,"categoryLabel":351,"publishedAt":410,"readingMinutes":120,"featured":383,"tags":411},"blog\u002Fblog\u002Freact-small-components.md","\u002Fblog\u002Freact-small-components","把重复的界面拆成小组件","从重复的按钮和卡片开始，认识组件拆分的几个简单信号。","2026-07-16",[351,404,412],"练习记录",{"id":414,"path":415,"title":416,"description":417,"categorySlug":352,"categoryLabel":351,"publishedAt":418,"readingMinutes":125,"featured":383,"tags":419},"blog\u002Fblog\u002Freact-use-effect-notes.md","\u002Fblog\u002Freact-use-effect-notes","React useEffect 的依赖和清理","整理 useEffect 何时运行，以及订阅和定时器为什么需要清理。","2026-06-30",[351,420,362],"useEffect",{"id":422,"path":423,"title":424,"description":425,"categorySlug":352,"categoryLabel":351,"publishedAt":426,"readingMinutes":83,"featured":383,"tags":427},"blog\u002Fblog\u002Freact-use-state-practice.md","\u002Fblog\u002Freact-use-state-practice","用 useState 管理一个简单的计数交互","从加一和重置两个操作，熟悉 React state 的声明与更新方式。","2026-07-30",[351,428,412],"useState",{"id":430,"path":431,"title":432,"description":433,"categorySlug":434,"categoryLabel":435,"publishedAt":436,"readingMinutes":120,"featured":63,"tags":437},"blog\u002Fblog\u002Ftypescript-component-props.md","\u002Fblog\u002Ftypescript-component-props","给 React 组件的 Props 添加 TypeScript 类型","用必填属性和可选属性描述组件接口，让使用方式在编辑器里更清晰。","typescript","TypeScript","2026-09-02",[435,351,438],"类型",{"id":440,"path":441,"title":442,"description":443,"categorySlug":434,"categoryLabel":435,"publishedAt":444,"readingMinutes":120,"featured":63,"tags":445},"blog\u002Fblog\u002Ftypescript-narrowing.md","\u002Fblog\u002Ftypescript-narrowing","TypeScript 类型收窄入门","使用 typeof 和条件判断缩小联合类型的范围，让后续代码更安全。","2026-09-25",[435,446,362],"类型收窄",{"id":448,"path":449,"title":450,"description":451,"categorySlug":434,"categoryLabel":435,"publishedAt":452,"readingMinutes":83,"featured":383,"tags":453},"blog\u002Fblog\u002Ftypescript-union-status.md","\u002Fblog\u002Ftypescript-union-status","用联合类型描述请求状态","把加载、成功和失败等状态限制在明确的集合里，减少拼写错误和遗漏分支。","2026-08-04",[435,454,362],"联合类型",{"id":456,"path":457,"title":458,"description":459,"categorySlug":460,"categoryLabel":461,"publishedAt":462,"readingMinutes":83,"featured":383,"tags":463},"blog\u002Fblog\u002Fvite-react-project.md","\u002Fblog\u002Fvite-react-project","用 Vite 创建一个 React + TypeScript 小项目","记录从初始化模板到启动开发服务器的基础步骤，以及几个值得先检查的文件。","tools","开发工具","2026-06-12",[464,351,435],"Vite",1792674168121]