本章覆盖从入门到可维护组件设计的完整基础。目标不是只会写 JSX,而是理解 React 如何把 JavaScript 状态映射为 UI,并能设计清晰、稳定、可演进的组件边界。
1. React 的核心模型
React 的核心思想可以概括为:
UI = f(state)你描述某个状态下 UI 应该是什么样,React 负责根据状态变化重新计算 UI,并把差异提交到浏览器 DOM。
传统 DOM 代码常写成步骤:
button.addEventListener('click',()=>{count+=1;label.textContent=count;button.disabled=count>10;});React 更关注结果:
function Counter() { const [count, setCount] = useState(0); return ( <button disabled={count > 10} onClick={() => setCount(count + 1)}> {count} </button> ); }2. JSX 与 React Element
JSX 是 JavaScript 的语法扩展,会被编译为 React Element。
const element = <h1 className="title">React</h1>;概念上类似:
constelement=React.createElement('h1',{className:'title'},'React',);JSX 中可以放表达式:
function Greeting({ user }) { return <h1>你好,{user.name}</h1>; }不能直接放语句:
// 错误 return <div>{if (ok) 'yes'}</div>;正确:
return <div>{ok ? 'yes' : 'no'}</div>;3. 组件函数
组件是接收 Props、返回 UI 描述的函数。
function KnowledgeCard({ title, summary, level }) { return ( <article> <span>{level}</span> <h3>{title}</h3> <p>{summary}</p> </article> ); }组件必须首字母大写:
<KnowledgeCard title="组件模型" />小写标签会被 React 当作 DOM 标签:
<div /> <button />不要直接调用组件函数:
// 错误 return <Layout>{Article()}</Layout>;正确:
return ( <Layout> <Article /> </Layout> );React 需要自己调用组件,才能管理 Hook、调度、Reconciliation 和开发检查。
4. Props
Props 是父组件传给子组件的只读输入。
function CourseList({ courses, onSelect }) { return ( <section> {courses.map((course) => ( <button key={course.id} onClick={() => onSelect(course.id)}> {course.title} </button> ))} </section> ); }Props 设计原则:
- 用业务语义命名:
onComplete比onClick更清楚。 - 子组件不修改 Props。
- 不随意透传大对象。
- 回调表达“发生了什么”,不是“怎么处理”。
推荐:
<CourseCard course={course} onComplete={completeCourse} />不推荐:
<Card data={course} onClick={handleClick} />5. State
State 是组件的记忆。
function Toggle() { const [open, setOpen] = useState(false); return ( <button onClick={() => setOpen((value) => !value)}> {open ? '收起' : '展开'} </button> ); }当新状态依赖旧状态时,使用函数式更新:
setCount((current) => current + 1);不要直接修改对象或数组:
// 错误 user.name = 'Ada'; setUser(user);正确:
setUser((user) => ({ ...user, name: 'Ada', }));数组更新:
setItems((items) => [...items, newItem]); setItems((items) => items.filter((item) => item.id !== id)); setItems((items) => items.map((item) => item.id === id ? { ...item, done: !item.done } : item, ), );6. 条件渲染
function StatusView({ status }) { if (status === 'loading') return <p>加载中...</p>; if (status === 'error') return <p>加载失败</p>; return <p>加载完成</p>; }短条件:
{error && <p role="alert">{error}</p>}二选一:
{isLoggedIn ? <Dashboard /> : <LoginPage />}7. 列表渲染与 key
function CourseList({ courses }) { return ( <section> {courses.map((course) => ( <CourseCard key={course.id} course={course} /> ))} </section> ); }key表示稳定身份。优先使用业务 ID,不要在会排序、插入、删除的列表中使用数组索引。
错误:
{items.map((item, index) => ( <EditableRow key={index} item={item} /> ))}后果:
- 输入框内容错位。
- 展开状态跑到另一行。
- 动画异常。
- React 错误复用组件实例。
8. 事件处理
React 事件使用驼峰命名:
<button onClick={handleClick}>保存</button>传参:
<button onClick={() => deleteItem(item.id)}>删除</button>阻止默认行为:
function submit(event) { event.preventDefault(); }React 事件接近浏览器事件,但由 React 统一管理。
9. 表单基础
受控表单由 React State 控制:
function LessonForm({ onSubmit }) { const [form, setForm] = useState({ title: '', level: '入门', }); function updateField(field, value) { setForm((current) => ({ ...current, [field]: value, })); } function submit(event) { event.preventDefault(); onSubmit(form); } return ( <form onSubmit={submit}> <input value={form.title} onChange={(event) => updateField('title', event.target.value)} /> <select value={form.level} onChange={(event) => updateField('level', event.target.value)} > <option>入门</option> <option>进阶</option> <option>高级</option> <option>精通</option> <option>专家</option> </select> <button disabled={!form.title.trim()}>保存</button> </form> ); }可派生的校验不要重复存 State:
const titleError = form.title.trim() ? '' : '标题不能为空'; const canSubmit = !titleError;10. children 与组合
children让组件成为结构容器。
function Panel({ title, children }) { return ( <section className="panel"> <h2>{title}</h2> <div>{children}</div> </section> ); } function Page() { return ( <Panel title="学习概览"> <p>今天完成 3 个知识点。</p> <button>继续学习</button> </Panel> ); }显式插槽:
function Modal({ title, children, footer }) { return ( <section role="dialog"> <header>{title}</header> <main>{children}</main> <footer>{footer}</footer> </section> ); }11. Fragment、StrictMode、内置组件
Fragment 用于不增加 DOM 节点地组合多个元素:
return ( <> <Header /> <Main /> </> );需要 key 时:
items.map((item) => ( <Fragment key={item.id}> <dt>{item.title}</dt> <dd>{item.summary}</dd> </Fragment> ));StrictMode 用于开发检查:
<React.StrictMode> <App /> </React.StrictMode>它会帮助发现不纯渲染、Effect 清理问题、废弃 API 等。开发中 Effect 看似执行两次时,通常应该修复副作用,而不是移除 StrictMode。
12. 组件职责分类
页面组件:负责页面编排。
function KnowledgePage() { return ( <PageLayout> <KnowledgeHub /> </PageLayout> ); }容器组件:负责状态、数据、行为。
function KnowledgeHubContainer() { const [state, dispatch] = useReducer(reducer, initialState); const visibleItems = selectVisibleItems(state); return ( <KnowledgeHubView state={state} items={visibleItems} dispatch={dispatch} /> ); }展示组件:根据 Props 渲染。
function KnowledgeCard({ item, favorite, onFavorite }) { return ( <article> <h3>{item.title}</h3> <p>{item.summary}</p> <button onClick={() => onFavorite(item.id)}> {favorite ? '取消收藏' : '收藏'} </button> </article> ); }13. 受控与非受控组件
受控组件:
function Tabs({ value, onChange, items }) { return ( <div> {items.map((item) => ( <button key={item.value} aria-selected={value === item.value} onClick={() => onChange(item.value)} > {item.label} </button> ))} </div> ); }非受控组件:
function Collapsible({ title, children }) { const [open, setOpen] = useState(false); return ( <section> <button onClick={() => setOpen((value) => !value)}> {title} </button> {open && children} </section> ); }组件库基础控件更适合受控,局部业务交互可以非受控。
14. 复合组件模式
适合 Tabs、Menu、Accordion 等一组协作组件。
const TabsContext = createContext(null); function TabsRoot({ value, onChange, children }) { return ( <TabsContext.Provider value={{ value, onChange }}> {children} </TabsContext.Provider> ); } function TabsTrigger({ value, children }) { const tabs = useContext(TabsContext); return ( <button aria-selected={tabs.value === value} onClick={() => tabs.onChange(value)} > {children} </button> ); }使用:
<TabsRoot value={tab} onChange={setTab}> <TabsTrigger value="overview">总览</TabsTrigger> <TabsTrigger value="demo">Demo</TabsTrigger> </TabsRoot>15. Headless 组件
把逻辑和样式分离。
function useDisclosure() { const [open, setOpen] = useState(false); return { open, show: () => setOpen(true), hide: () => setOpen(false), toggle: () => setOpen((value) => !value), }; }业务组件:
function DeleteDialog() { const dialog = useDisclosure(); return ( <> <button onClick={dialog.show}>删除</button> {dialog.open && <ConfirmModal onClose={dialog.hide} />} </> ); }16. 组件抽象时机
可以抽象的信号:
- 重复出现三次以上。
- 业务含义稳定。
- 变化点可以用少量 Props 表达。
- 抽象后调用方更容易理解。
不该抽象的信号:
- 只是样式碰巧相似。
- 每个使用点都有大量特殊逻辑。
- Props 数量不断膨胀。
- 为了复用牺牲可读性。
17. 入门到高级达标标准
入门:
- 能写 JSX、组件、Props、State、事件、列表和表单。
- 能避免直接修改 State。
- 能使用稳定 key。
进阶:
- 能设计组件职责。
- 能区分受控和非受控。
- 能用组合代替继承。
高级:
- 能判断组件边界是否表达变化边界。
- 能避免布尔 Props 爆炸。
- 能把业务规则从展示组件中抽离。
专家:
- 能设计组件库 API。
- 能制定组件抽象准入标准。
- 能让组件系统支持长期业务演进。
18. React 基础知识点扩展清单
这一节补齐入门阶段容易遗漏的细节。React Element 是描述 UI 的普通对象,Component 是产生 Element 的函数,DOM instance 是 React DOM 在浏览器中维护的真实节点。理解三者区别,才能明白为什么不要直接调用组件函数,也不要在渲染期间操作 DOM。
组件必须保持纯粹。错误示例:
let nextId = 0; function Item() { nextId += 1; return <div>{nextId}</div>; }正确做法是把变化来源放进 Props、State 或 Context:
function Item({ id }) { return <div>{id}</div>; }Props 默认值适合展示层兜底,但不适合掩盖业务数据缺失:
function Avatar({ size = 40, src, alt = '用户头像' }) { return <img width={size} height={size} src={src} alt={alt} />; }children可以是字符串、数字、元素、数组、null或条件渲染结果。条件渲染要避免0被渲染:
{items.length > 0 && <List items={items} />}组件命名要表达业务或 UI 角色:KnowledgeCard、PermissionGate、UserAvatar都比Box2、CommonCard、LeftPart更可维护。
19. 组件模式扩展
Container + View 模式适合复杂页面:
function CoursePageContainer() { const query = useCourses(); const [selectedId, setSelectedId] = useState(null); return ( <CoursePageView status={query.status} courses={query.data} selectedId={selectedId} onSelect={setSelectedId} /> ); }受控/非受控混合模式适合组件库:
function Toggle({ checked, defaultChecked = false, onCheckedChange }) { const [innerChecked, setInnerChecked] = useState(defaultChecked); const isControlled = checked !== undefined; const value = isControlled ? checked : innerChecked; function update(next) { if (!isControlled) setInnerChecked(next); onCheckedChange?.(next); } return <button onClick={() => update(!value)}>{value ? '开' : '关'}</button>; }专家提醒:同时支持受控和非受控会增加复杂度,只有基础组件库值得这样做。
20. 基础练习库
- 课程卡片:标题、简介、层级、标签、收藏、禁用态、长文本截断。
- 筛选列表:搜索、层级筛选、空状态、稳定 key。
- 受控表单:标题必填、层级下拉、标签动态添加、提交按钮禁用。
- 组件重构:把 300 行页面拆成容器、筛选栏、列表、卡片、空状态、弹窗。
评审问题:
- 组件的状态是否应该提升?
- Props 是业务语义还是实现语义?
- key 在排序和删除后是否仍稳定?
- children 是否合适,还是应该显式 props?
- 是否存在直接修改对象或数组的更新?
22. 组件知识点索引
- JSX 只能返回一个根结构,可用 Fragment。
- JSX 中表达式用
{},语句需要提前计算。 - 组件必须大写,DOM 标签小写。
- Props 是只读输入。
- State 是渲染快照,不是可变变量。
- setState 调度下一次渲染,不是同步赋值。
- 对象和数组 State 必须不可变更新。
- key 表示稳定身份,不是消除 warning 的装饰。
- 事件回调表达用户意图。
- 条件渲染要避免
0、空字符串等误渲染。 - children 是组合机制。
- 组件函数不能直接调用。
- 组件渲染应保持纯粹。
- StrictMode 用于暴露潜在问题。
- 组件拆分应围绕职责和变化方向。
- 展示组件不应知道数据来源。
- 容器组件不应充满视觉细节。
- 页面组件负责编排,不负责所有业务规则。
- 受控组件适合外部协调。
- 非受控组件适合局部交互。
- 复合组件适合稳定结构的复杂 UI。
- Headless Hook 适合抽离交互逻辑。
- Render props 能表达动态渲染,但会增加嵌套。
- 高阶组件历史上常见,现在更多用 Hook。
- Error Boundary 仍需 class 或框架封装。
- Portal 改变 DOM 位置,不改变 React 事件冒泡树。
- ref 是逃生口,不是状态管理工具。
- 表单字段错误通常可派生。
- 组件库 API 要少暴露内部实现。
- 组件抽象要有废弃策略。
23. 组件反模式大全
23.1 布尔开关爆炸
<Card isAdmin isCompact isNew isDanger hasFooter />问题是状态组合不可控。更好拆组件或使用variant。
23.2 万能组件
<CommonRenderer type="course" mode="edit" source="remote" />万能组件通常没人敢改。业务组件应保持明确边界。
23.3 过早组件库化
第一次重复就抽象,容易产生大量半通用组件。更稳妥的策略是先允许局部重复,再在第三次出现稳定模式时抽象。
23.4 UI 和领域规则混写
disabled={user.role !== 'admin' || order.status !== 'draft' || locked}更好:
disabled={!canEditOrder(user, order, locked)}23.5 props drilling 过深
如果中间 5 层组件只负责转发 Props,应考虑组合、Context 或重新设计组件边界。
24. 基础阶段面试题
- React Element 和 Component 有什么区别?
- 为什么组件不能直接当函数调用?
- 为什么 State 更新要不可变?
- key 为什么必须稳定?
- children 和 render props 的区别是什么?
- 什么时候抽组件,什么时候保留重复?
- 受控组件和非受控组件如何选择?
- StrictMode 下 Effect 执行两次意味着什么?
面试题完整答案总集:React 基础与组件
React Element 和 Component 有什么区别?
React Element 是描述 UI 的普通 JavaScript 对象,例如<Button />编译后的结果;Component 是产生 Element 的函数或类,例如function Button() { return <button /> }。Element 是“结果描述”,Component 是“生成描述的逻辑”。React 调用 Component 得到 Element,再交给 renderer 更新 DOM 或其他目标平台。
为什么组件不能直接当函数调用?
直接调用组件函数会绕过 React 的渲染流程,React 无法正确关联 Hook 调用顺序、维护组件身份、调度更新、执行 StrictMode 检查和记录 DevTools 信息。正确方式是使用<Component />,让 React 接管组件调用。
为什么 State 更新要不可变?
React 依赖引用变化来判断对象或数组是否更新。直接修改旧对象会让新旧引用相同,可能导致组件不更新、memo 判断错误、旧状态快照被污染。不可变更新能保留历史快照,也更利于调试、撤销、测试和性能优化。
key 为什么必须稳定?
key 用来表示列表项身份。稳定 key 能让 React 正确复用组件实例和内部状态。使用数组索引作为 key 时,插入、删除或排序会让身份错位,导致输入框内容、展开状态或动画状态跑到错误行。
children 和 render props 的区别是什么?
children 适合静态组合和结构插槽,例如<Panel><Content /></Panel>。render props 是把函数传给组件,让组件用内部状态调用函数生成 UI。render props 更灵活,但嵌套和理解成本更高;现代 React 中很多 render props 场景可用自定义 Hook 替代。
什么时候抽组件,什么时候保留重复?
当 UI 有稳定业务含义、重复出现多次、变化点清晰、抽出后调用方更容易理解时,应该抽组件。若只是样式相似、每处逻辑差异很大、Props 不断膨胀,就应保留局部重复。好的抽象减少复杂度,坏的抽象只是隐藏复杂度。
受控组件和非受控组件如何选择?
受控组件由外部状态控制,适合表单校验、字段联动、外部重置和组件库控件。非受控组件自己管理状态,适合局部展开、简单开关、无需外部协调的交互。基础组件库常支持受控,业务局部组件优先选择简单方案。
StrictMode 下 Effect 执行两次意味着什么?
开发环境下 StrictMode 会额外执行某些渲染和 Effect 流程,用来暴露副作用不纯或清理不完整的问题。这不是生产行为,也不是 React bug。正确做法是让 Effect 可重复同步并正确清理,例如取消订阅、清除定时器、取消请求或忽略过期结果。
组件的状态是否应该提升?
看状态的使用范围。只有当前组件使用就留在当前组件;兄弟组件共享就提升到最近公共父组件;跨页面或跨业务共享再考虑 Context、状态库或服务端缓存。状态放得越高,影响范围越大,不应为了方便过度提升。
Props 是业务语义还是实现语义?
业务语义描述组件能力和用户意图,如onComplete、variant="danger"。实现语义暴露内部细节,如isRed、useNewFooter。组件 API 应优先使用业务语义,这样内部实现变化时调用方不需要跟着修改。