React 库 练习题(10 题)
这一页给 10 道 React 入门练习题,全程使用函数组件 + Hooks(不含 class 组件),覆盖JSX、props、useState、事件处理、条件渲染、列表 key、useEffect、受控表单、children 复用、待办清单实战。每题都有题目描述、提示、可折叠的参考答案。
使用建议:React 的核心思想是「数据驱动视图 + 单向数据流」。先理解 props/state 的区别,再掌握 useState/useEffect 两个最常用的 Hook。每题先自己写,卡住再看提示,实在写不出对照答案。函数组件 + Hooks 是现代 React 的主流写法,新项目都用它。
基础(5 题):组件 / props / state / 事件 / 条件
第 1 题:函数组件与 JSX(第一个组件)
题目:写一个 Welcome 函数组件,返回一个 h1 显示「你好,React!」。再写一个 App 组件,在内部使用两次 <Welcome />。
提示:组件名必须大写(小写会被当成 HTML 标签);必须返回一段 JSX;JSX 是「JS 里写类 HTML」,只能有一个根元素(或用 <>...</> Fragment)。
参考答案
import React from 'react';
// 函数组件:首字母大写,返回一段 JSX
function Welcome() {
return <h1>你好,React!</h1>;
}
// 在父组件里使用(像 HTML 标签一样)
function App() {
return (
<div>
<Welcome />
<Welcome />
</div>
);
}
export default App;
// 知识点:
// - 组件名必须大写(Welcome),小写(welcome)会被当成 HTML 标签
// - 必须返回一段 JSX(或 null)
// - JSX = JS 里写类 HTML 语法,编译后是 React.createElement
// - 一个组件一个文件更清晰,export default 导出第 2 题:props 传参
题目:写一个 Greeting 组件,接收 name(必填)、age(默认 18)、hobby(可选,有就显示)三个 props。在父组件里调用三次,传不同的值。
提示:props 是只读对象,绝不能在子组件里改;字符串直接写 name="张三",数字/表达式用 {`{}`} 包裹 {`age={30}`};默认值写在解构里;一次性透传用 {`{...user}`}。
参考答案
// 解构写法 + 默认值,清晰且健壮
function Greeting({ name, age = 18, hobby }) {
return (
<p>
你好,{name},今年 {age} 岁。
{hobby && <span> 爱好:{hobby}。</span>}
</p>
);
}
// 使用:不同父组件传不同数据
function App() {
return (
<>
<Greeting name="张三" />
<Greeting name="李四" age={30} />
<Greeting name="王五" age={22} hobby="游泳" />
</>
);
}
// 知识点:
// - props 是只读对象,绝不能在子组件里改(props.name = 'x' ❌)
// - 字符串直接写 name="张三";数字/布尔/表达式用 {} 包裹 age={30}
// - 默认值写在解构里,调用时不传则用默认
// - 一次性透传所有属性:<Greeting {...user} />第 3 题:useState 状态管理
题目:写一个计数器 Counter,有「+步长 / -步长 / 重置」三个按钮,步长可通过下拉框调整(1/5/10)。再演示「新值依赖旧值」时如何安全更新。
提示:const [count, setCount] = useState(0);不能直接 count++,必须 setCount 触发重渲染;新值依赖旧值时用函数式更新 {`setCount(c => c + 1)`},避免批处理竞态。状态是独立的,渲染两个 Counter 各管各的。
参考答案
import { useState } from 'react';
function Counter() {
// 解构:[当前值, 修改函数],括号里是初始值
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return (
<div>
<p>当前:{count}(步长 {step})</p>
<button onClick={() => setCount(count + step)}>+{step}</button>
<button onClick={() => setCount(count - step)}>-{step}</button>
{/* 新值依赖旧值时,推荐函数式更新,避免批处理竞态 */}
<button onClick={() => setCount(c => c + 1)}>函数式 +1</button>
<button onClick={() => setCount(0)}>重置</button>
<select value={step} onChange={e => setStep(Number(e.target.value))}>
<option value={1}>步长 1</option>
<option value={5}>步长 5</option>
<option value={10}>步长 10</option>
</select>
</div>
);
}
// 知识点:
// - 不能直接 count++ —— 必须 setCount 触发重新渲染
// - 新值依赖旧值用函数式:setCount(c => c + 1),更安全
// - 状态是独立的:渲染两个 <Counter /> 各管各的
// - 初始值昂贵(读 localStorage 等)时传函数:useState(() => loadX())第 4 题:事件处理(onClick / onChange / onSubmit)
题目:写一个搜索框,有 input + 提交按钮,提交后把关键词加入历史列表(保留最近 5 条)。每个历史项可点击「×」删除。要求:表单提交用 onSubmit,删除按钮要传索引参数。
提示:事件用 on + 大驼峰(onClick/onChange/onSubmit);表单记得 e.preventDefault() 防止刷新;给回调传额外参数必须用箭头函数包一层,否则会在渲染时立即执行({`onClick={() => remove(i)}`} 而不是 {`onClick={remove(i)}`})。
参考答案
import { useState } from 'react';
function SearchBox() {
const [keyword, setKeyword] = useState('');
const [history, setHistory] = useState([]);
// 直接写在 JSX 里(短小逻辑)
// <input onChange={e => setKeyword(e.target.value)} />
// 抽成独立函数(逻辑复杂或复用时)
const handleSearch = (e) => {
e.preventDefault(); // 阻止表单默认提交刷新
if (!keyword.trim()) return;
setHistory([keyword, ...history].slice(0, 5)); // 保留最近 5 条
setKeyword('');
console.log('搜索:', keyword);
};
// 给列表项传参:用箭头包一层,避免立即执行
const removeHistory = (idx) => {
setHistory(history.filter((_, i) => i !== idx));
};
return (
<form onSubmit={handleSearch}>
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="输入关键词"
/>
<button type="submit">搜索</button>
<ul>
{history.map((h, i) => (
<li key={i}>
{h}
{/* ❌ onClick={removeHistory(i)} 会在渲染时立即执行 */}
{/* ✅ 用箭头函数包一层,延迟到点击时执行 */}
<button onClick={() => removeHistory(i)}>×</button>
</li>
))}
</ul>
</form>
);
}
// 知识点:
// - 事件用 on + 大驼峰:onClick / onChange / onSubmit
// - 合成事件 SyntheticEvent,API 和原生 DOM 类似
// - onChange 在 input 上每次键入都触发(不像原生要失焦)
// - 给回调传额外参数,用箭头函数包一层,否则会立即调用第 5 题:条件渲染(&& 与三元)
题目:写一个 UserPanel,根据 user.isLoggedIn 显示欢迎语、根据 user.isAdmin 显示不同按钮。再写一个 Demo 演示「数字 0」会渲染出来的经典坑,并给出正确写法。
提示:&& 用于「有就显示」,三元用于「二选一」;别把数字直接 && —— 0 是假值但会被渲染出来({`count && <X />`} 当 count=0 时显示「0」);正确写法 {`count > 0 && <X />`} 或用三元强制返回 null。
参考答案
import { useState } from 'react';
function UserPanel({ user }) {
return (
<div>
{/* 方式 1:&& 短路 —— 条件为真才渲染 */}
{user.isLoggedIn && <p>欢迎回来,{user.name}!</p>}
{/* 方式 2:三元表达式 —— 二选一 */}
{user.isAdmin
? <button>进入管理后台</button>
: <button>进入个人中心</button>}
{/* 方式 3:提前 return(整块分支) */}
{!user.isLoggedIn && <p>请先登录</p>}
</div>
);
}
function Demo() {
const [count, setCount] = useState(0);
return (
<div>
{/* ⚠️ 大坑:count && <X /> —— count 为 0 时会渲染出 "0" 字样!
因为 && 返回左侧操作数,0 是假值但会被渲染 */}
<p>错误写法可能显示 0:{count}</p>
{/* ✅ 正确:把比较结果做条件,或用三元强制返回 null */}
{count > 0 && <p>有 {count} 条消息</p>}
{count === 0 ? <p>暂无消息</p> : <p>共 {count} 条</p>}
</div>
);
}
// 知识点:
// - && 用于「有就显示」,三元用于「二选一」
// - 别把数字直接 && —— 0 会渲染出来,新手最常踩的坑
// - 三元表达式可以嵌套,但太长就抽成函数或子组件
// - 返回 null 表示「什么都不渲染」进阶(5 题):列表 / useEffect / 表单 / children / 实战
第 6 题:列表渲染(.map + key)
题目:渲染一个水果列表(id/name/price),每项可删除。再写一个嵌套树形列表(节点有 children),演示嵌套时 key 怎么加。
提示:.map 把数组转 JSX;key 必须加且唯一稳定,绝不用数组下标 index(增删/排序时 React 会认错节点,导致状态错乱);key 在兄弟节点间唯一即可;嵌套列表外层和内层都要加 key。
参考答案
import { useState } from 'react';
function FruitList() {
const [fruits, setFruits] = useState([
{ id: 'f1', name: '苹果', price: 5 },
{ id: 'f2', name: '香蕉', price: 3 },
{ id: 'f3', name: '橘子', price: 4 },
]);
return (
<ul>
{/* .map 把数组转成 JSX 数组,key 必须加且稳定唯一 */}
{fruits.map(f => (
<li key={f.id}>
<strong>{f.name}</strong> - ¥{f.price}
<button onClick={() =>
setFruits(fruits.filter(x => x.id !== f.id))
}>删除</button>
</li>
))}
</ul>
);
}
// 嵌套列表:外层和内层都要 key
function TreeView({ data }) {
return (
<ul>
{data.map(node => (
<li key={node.id}>
{node.title}
{node.children && (
<ul>
{node.children.map(child => (
<li key={child.id}>{child.title}</li>
))}
</ul>
)}
</li>
))}
</ul>
);
}
// 知识点:
// - key 必须加,且唯一稳定 —— 别用数组下标 index
// - 用 index 作 key 在增删/排序时会错乱,React diff 会认错节点
// - key 在兄弟节点间唯一即可,全局不需要唯一
// - 数组里写 JSX,模板里用 {} 包 map 表达式第 7 题:useEffect 副作用(定时器 / 数据获取)
题目:写两个组件:(a) Clock 每秒更新一次时间,卸载时清理定时器;(b) Profile 接收 userId,userId 变化时重新 fetch 用户数据,且组件卸载后不再 setState。
提示:第二参数依赖数组决定何时重跑 —— {`[]`} 只在挂载时跑,{`[userId]`} 在 userId 变化时跑;有订阅/定时器/事件监听就必须 return 清理函数;依赖别漏写,否则闭包里读到旧值(stale closure);异步请求要处理「组件已卸载」的情况,避免内存泄漏。
参考答案
import { useState, useEffect } from 'react';
// 示例 1:挂载时启动定时器,卸载时清理
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
console.log('effect 执行:订阅定时器');
const timer = setInterval(() => setNow(new Date()), 1000);
// 返回清理函数:组件卸载 或 下次 effect 执行前调用
return () => {
console.log('清理定时器');
clearInterval(timer);
};
}, []); // 空依赖数组 = 只在挂载时跑一次
return <p>现在:{now.toLocaleTimeString()}</p>;
}
// 示例 2:依赖变化时重新跑
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch('/api/users/' + userId)
.then(r => r.json())
.then(data => {
if (!cancelled) setUser(data); // 防止卸载后还 setState
});
return () => { cancelled = true; }; // 标记取消,丢弃过期响应
}, [userId]); // userId 变了才重新获取
return <div>{user ? user.name : '加载中...'}</div>;
}
// 知识点:
// - 第二参数依赖数组:[] 只跑一次,[x, y] 在 x/y 变化时跑
// - 有订阅/定时器/事件监听,就必须 return 清理函数
// - 依赖别漏写,否则闭包里读到旧的 state/props(stale closure)
// - 异步请求要处理「组件已卸载」的情况,避免内存泄漏警告第 8 题:表单受控组件
题目:写一个联系表单,包含:text input、email input、textarea、checkbox(订阅)、select(性别)。用一个对象 state 管理所有字段,共用一个 handleChange。提交时打印表单数据。
提示:受控 = value 来自 state、onChange 写回 state,数据流单向;多字段用计算属性 {`[name]: value`} 复用同一 handler;checkbox 用 checked,select 用 value(不是原生 selected);别忘了 e.preventDefault() 阻止默认提交。
参考答案
import { useState } from 'react';
function ContactForm() {
// 一个对象管理所有字段,避免一堆 useState
const [form, setForm] = useState({
name: '',
email: '',
msg: '',
subscribe: false,
gender: 'male',
});
// 计算属性名:[e.target.name]:value,所有字段共用一个 handler
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认提交刷新页面
console.log('提交:', form);
alert('已提交:\n' + JSON.stringify(form, null, 2));
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="姓名" />
<input name="email" type="email" value={form.email} onChange={handleChange} />
<textarea name="msg" value={form.msg} onChange={handleChange} rows={3} />
<label>
<input type="checkbox" name="subscribe" checked={form.subscribe}
onChange={handleChange} />
订阅每周邮件
</label>
<select name="gender" value={form.gender} onChange={handleChange}>
<option value="male">男</option>
<option value="female">女</option>
<option value="other">其他</option>
</select>
<button type="submit">发送</button>
</form>
);
}
// 知识点:
// - 受控 = value 来自 state,onChange 写回 state,数据流单向
// - 多字段用计算属性 [name]:value 复用同一 handler
// - checkbox 用 checked,select 用 value(不是 selected)
// - 别忘了 e.preventDefault() 阻止表单默认提交刷新整个页面第 9 题:组件复用与 children
题目:写两个通用容器组件:(a) Card 接收 title、可选 extra(右上角插槽)、children;(b) Collapse 可折叠面板,接收 title、defaultOpen、children。在 App 里组合使用,演示 children 可以是任意 JSX。
提示:children 让组件像容器一样组合,符合「组合优于继承」;适合做布局壳子(Card / Modal / Layout / Tabs);任意 JSX(文字、标签、组件、数组)都能作为 children 传入;需要多个「插槽」时用 props 传 ReactNode。
参考答案
import { useState } from 'react';
// 通用「卡片」容器:title + 自定义内容
function Card({ title, extra, children }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
{extra && <div className="card-extra">{extra}</div>}
</div>
<div className="card-body">{children}</div>
</div>
);
}
// 通用「可折叠」组件
function Collapse({ title, children, defaultOpen = false }) {
const [open, setOpen] = useState(defaultOpen);
return (
<div>
<button onClick={() => setOpen(!open)}>
{open ? '▼' : '▶'} {title}
</button>
{open && <div className="collapse-body">{children}</div>}
</div>
);
}
// 使用:children 是标签之间的内容,任意 JSX 都能传
function App() {
return (
<>
<Card title="通知" extra={<a href="/more">更多</a>}>
<p>系统今晚 22:00 维护,请提前保存。</p>
<button>我知道了</button>
</Card>
<Card title="统计">
<ul>
<li>用户:1024</li>
<li>订单:256</li>
</ul>
</Card>
<Collapse title="高级选项">
<input placeholder="自定义配置" />
<select><option>A</option><option>B</option></select>
</Collapse>
</>
);
}
// 知识点:
// - children 让组件像「容器」一样组合,符合「组合优于继承」
// - 适合做布局壳子:Card / Modal / Layout / Collapse / Tabs
// - 任意 JSX(文字、标签、组件、数组)都能作为 children 传入
// - 一个组件还能接收多个「插槽」:用 props 传 ReactNode(extra)第 10 题:实战 — 待办清单组件(增删改 + localStorage)
题目:综合实战。写一个 Todos 组件,功能:(a) 添加(回车或按钮);(b) 切换完成状态;(c) 编辑(双击或编辑按钮);(d) 删除;(e) 三种过滤(全部/未完成/已完成)和剩余计数;(f) 自动持久化到 localStorage,刷新不丢。拆出 TodoItem 子组件。
提示(综合要点):useState 惰性初始化读 localStorage;useEffect 监听 todos 变化自动持久化;不可变更新(展开/map/filter,绝不原地改);key 用稳定 id(Date.now()),绝不用 index;派生数据(剩余数/过滤结果)用普通变量,不要再存 state;父组件管数据、子组件管交互触发回调(单向数据流)。
参考答案
import { useState, useEffect } from 'react';
function Todos() {
// 惰性初始化:函数只在首次渲染执行一次,避免每次渲染都读 localStorage
const [todos, setTodos] = useState(() => {
try {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
} catch {
return []; // 容错:JSON 解析失败也别崩
}
});
const [input, setInput] = useState('');
const [filter, setFilter] = useState('all'); // all / active / done
// 每次 todos 变化自动持久化(订阅模型)
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const add = () => {
const text = input.trim();
if (!text) return;
// 不可变更新:用新数组,别 push 原数组
setTodos([{ id: Date.now(), text, done: false }, ...todos]);
setInput('');
};
const toggle = (id) =>
setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
const del = (id) =>
setTodos(todos.filter(t => t.id !== id));
const edit = (id, text) =>
setTodos(todos.map(t => t.id === id ? { ...t, text } : t));
// 派生数据:从已有 state 计算,不要再存一个 state
const visible = todos.filter(t =>
filter === 'all' ? true :
filter === 'active' ? !t.done :
t.done
);
const remaining = todos.filter(t => !t.done).length;
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && add()}
placeholder="新待办..."
/>
<button onClick={add}>添加</button>
<div>
{['all', 'active', 'done'].map(f => (
<button key={f} onClick={() => setFilter(f)}
style={{ fontWeight: filter === f ? 'bold' : 'normal' }}>
{f === 'all' ? '全部' : f === 'active' ? '未完成' : '已完成'}
</button>
))}
<span>剩余 {remaining} 项</span>
</div>
<ul>
{visible.map(t => (
<TodoItem key={t.id} todo={t}
onToggle={toggle} onDel={del} onEdit={edit} />
))}
</ul>
</div>
);
}
// 子组件:单条待办(把交互通过回调传出去,符合单向数据流)
function TodoItem({ todo, onToggle, onDel, onEdit }) {
const [editing, setEditing] = useState(false);
const [text, setText] = useState(todo.text);
const save = () => {
onEdit(todo.id, text.trim());
setEditing(false);
};
return (
<li style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
<input type="checkbox" checked={todo.done}
onChange={() => onToggle(todo.id)} />
{editing ? (
<>
<input value={text} onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === 'Enter' && save()} autoFocus />
<button onClick={save}>保存</button>
</>
) : (
<>
<span onDoubleClick={() => setEditing(true)}>{todo.text}</span>
<button onClick={() => setEditing(true)}>编辑</button>
</>
)}
<button onClick={() => onDel(todo.id)}>删除</button>
</li>
);
}
// 知识点(综合实战):
// - useState 惰性初始化:传函数,只在首次渲染跑一次
// - useEffect + localStorage 实现自动持久化,刷新不丢
// - 不可变更新:...展开 / map / filter,绝不原地改 state
// - key 用稳定 id(Date.now()),绝不用 index
// - 派生数据(剩余数 / 过滤结果)用普通变量,不要存到 state
// - 父组件管数据,子组件管交互 + 触发回调,单向数据流练习建议
React 的核心是「UI = f(state)」 —— 数据决定视图,你只管改 state,React 帮你算出该渲染什么。重点掌握函数组件 + 两个最常用 Hook:useState(管数据)、useEffect(管副作用)。这两个用熟了,80% 的场景都能搞定。
几个最容易踩的坑:(1) 直接改 state 而不是用 set 函数;(2) key 用 index 导致列表错乱;(3) useEffect 漏写依赖导致闭包读到旧值;(4) 数字 0 用 && 渲染出「0」;(5) 给回调传参没用箭头函数包,渲染时立即执行。做完这 10 题,基本就能写出像样的 React 小应用了。
← 上一篇 React 实战项目
返回 React 教程目录