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 教程目录

✈️💬