Skip to content

Latest commit

 

History

History
1087 lines (890 loc) · 27.1 KB

02-react-typescript.md

File metadata and controls

1087 lines (890 loc) · 27.1 KB

React + TypeScript

第六部分 - 第 2 节 | 面向 Java 工程师的 React 入门


1. React 核心概念

什么是 React

React 是 Facebook(Meta)开发的前端 UI 库,核心思想是用组件化的方式构建用户界面。

Java 工程师的理解方式:

Java 后端渲染(你熟悉的):
  Controller 接收请求 → 准备数据 → 模板引擎(Thymeleaf)渲染 HTML → 返回给浏览器

React 前端渲染(新方式):
  浏览器加载 JS → React 组件根据数据生成虚拟 DOM → 渲染到页面
  数据变化时 → React 自动计算差异 → 只更新变化的部分

对比 Thymeleaf

<!-- Thymeleaf(Java 模板引擎)-->
<div th:each="user : ${users}">
  <span th:text="${user.name}">张三</span>
  <span th:text="${user.age}">25</span>
</div>
// React(前端组件)
function UserList({ users }: { users: User[] }) {
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          <span>{user.age}</span>
        </div>
      ))}
    </div>
  );
}

核心区别:

特性 Thymeleaf React
渲染位置 服务器端 浏览器端
模板语法 th:text, th:each JSX(HTML in JS)
交互 整页刷新或 AJAX 局部更新,无刷新
状态管理 服务端 Session 前端组件状态
数据绑定 单向(后端→模板) 单向数据流(state→UI)

2. JSX/TSX 语法

JSX 是什么

JSX(JavaScript XML)让你在 JavaScript/TypeScript 中直接写类似 HTML 的语法。TSX 就是支持 TypeScript 的 JSX。

// 这不是 HTML,而是 JSX!它会被编译成 JavaScript 函数调用
const element = <h1 className="title">Hello, World!</h1>;

// 编译后等价于:
const element = React.createElement('h1', { className: 'title' }, 'Hello, World!');

JSX 与 HTML 的差异

function Example() {
  return (
    <div>
      {/* JSX 与 HTML 的不同之处 */}

      {/* 1. class → className(因为 class 是 JS 的保留字) */}
      <div className="container">内容</div>

      {/* 2. for → htmlFor */}
      <label htmlFor="name">姓名</label>

      {/* 3. style 接受对象,不是字符串 */}
      <div style={{ color: 'red', fontSize: '16px' }}>红色文字</div>

      {/* 4. 所有标签必须闭合 */}
      <img src="logo.png" alt="Logo" />  {/* 必须自闭合 */}
      <br />

      {/* 5. 事件名用 camelCase */}
      <button onClick={() => alert('点击了')}>点击</button>

      {/* 6. 在 {} 中可以写任何 JavaScript 表达式 */}
      <p>当前时间: {new Date().toLocaleString()}</p>
      <p>1 + 1 = {1 + 1}</p>
    </div>
  );
}

Java 工程师理解方式: JSX 的 {} 类似 Thymeleaf 的 ${} —— 在模板中嵌入表达式。区别是 JSX 的 {} 里是真正的 JavaScript 表达式,不是模板语言。


3. 函数组件与 Props 类型

函数组件定义

React 组件就是一个返回 JSX 的函数。Props 是从父组件传入的参数。

// 方式一:直接定义 Props 接口(推荐)
interface GreetingProps {
  name: string;
  age?: number;  // 可选属性
}

function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      <h2>你好,{name}!</h2>
      {age !== undefined && <p>年龄:{age}</p>}
    </div>
  );
}

// 使用组件
<Greeting name="张三" />
<Greeting name="李四" age={28} />
// 方式二:使用 React.FC 类型(也常见)
interface CardProps {
  title: string;
  count?: number;
}

const Card: React.FC<CardProps> = ({ title, count = 0 }) => {
  return (
    <div className="card">
      <h3>{title}</h3>
      <span>数量: {count}</span>
    </div>
  );
};

Java 类比: React 组件 ≈ Java 类,Props ≈ 构造函数参数

// Java 类比
public class Greeting {
    private final String name;
    private final Integer age;  // 可选

    public Greeting(String name, @Nullable Integer age) {
        this.name = name;
        this.age = age;
    }

    public String render() {
        return "<h2>你好," + name + "!</h2>";
    }
}

Props 类型的常见模式

// 1. 必选和可选属性
interface ButtonProps {
  label: string;           // 必选
  variant?: 'primary' | 'secondary' | 'danger';  // 可选,用联合类型限制
  disabled?: boolean;      // 可选
  onClick: () => void;     // 必选的回调函数
}

function Button({ label, variant = 'primary', disabled = false, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// 使用
<Button label="提交" onClick={() => console.log('提交')} />
<Button label="删除" variant="danger" onClick={handleDelete} />
// 2. 泛型组件(类似 Java 的泛型类)
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 使用:TypeScript 自动推断泛型参数
<List
  items={['苹果', '香蕉', '橘子']}
  renderItem={(fruit) => <span>{fruit}</span>}  // fruit 推断为 string
/>

<List
  items={[{ id: 1, name: '张三' }, { id: 2, name: '李四' }]}
  renderItem={(user) => <span>{user.name}</span>}  // user 推断为 { id: number; name: string }
/>

children 的类型

children 是 React 中一个特殊的 Prop,表示组件标签之间的内容。

// React.ReactNode — 最通用的 children 类型
interface LayoutProps {
  children: React.ReactNode;  // 可以是任何可渲染的内容
  title: string;
}

function Layout({ children, title }: LayoutProps) {
  return (
    <div className="layout">
      <header><h1>{title}</h1></header>
      <main>{children}</main>
      <footer>版权所有</footer>
    </div>
  );
}

// 使用:children 就是标签之间的内容
<Layout title="首页">
  <p>这是页面内容</p>
  <button>点击我</button>
</Layout>

React.ReactNode 包含的类型:

// ReactNode 可以是以下任何一种
type ReactNode =
  | React.ReactElement    // <Component /> 或 <div />
  | string                // "文本"
  | number                // {42}
  | boolean               // {true}(不渲染)
  | null                  // {null}(不渲染)
  | undefined             // {undefined}(不渲染)
  | React.ReactNode[];    // 数组

4. Hooks 详解

Hooks 是 React 16.8 引入的特性,让函数组件可以拥有状态和副作用。

4.1 useState<T> — 状态管理

useState 是最基础的 Hook,用于在组件中管理状态。

import { useState } from 'react';

function Counter() {
  // useState 返回 [当前值, 更新函数] 的元组
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

Java 类比:

// Java 中的状态概念
public class Counter {
    private int count = 0;  // 状态

    // React 中 setCount(newValue) 不仅更新值,还会触发 UI 重新渲染
    public void increment() {
        this.count++;
        this.render();  // Java 中需要手动触发渲染
    }
}

复杂状态的类型:

interface FormData {
  username: string;
  email: string;
  role: 'admin' | 'user';
}

function UserForm() {
  // 泛型参数指定状态类型
  const [form, setForm] = useState<FormData>({
    username: '',
    email: '',
    role: 'user',
  });

  // 更新部分字段(展开运算符)
  const updateUsername = (name: string) => {
    setForm(prev => ({ ...prev, username: name }));
  };

  // 通用更新函数
  const updateField = <K extends keyof FormData>(key: K, value: FormData[K]) => {
    setForm(prev => ({ ...prev, [key]: value }));
  };

  return (
    <form>
      <input
        value={form.username}
        onChange={e => updateField('username', e.target.value)}
        placeholder="用户名"
      />
      <input
        value={form.email}
        onChange={e => updateField('email', e.target.value)}
        placeholder="邮箱"
      />
      <select
        value={form.role}
        onChange={e => updateField('role', e.target.value as FormData['role'])}
      >
        <option value="user">普通用户</option>
        <option value="admin">管理员</option>
      </select>
    </form>
  );
}

4.2 useEffect — 副作用

useEffect 用于处理"副作用":网络请求、订阅、DOM 操作、定时器等。

import { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  // useEffect 在组件渲染后执行
  useEffect(() => {
    // 异步请求数据
    async function fetchUser() {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data: User = await response.json();
        setUser(data);
      } catch (error) {
        console.error('获取用户失败:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]); // 依赖数组:userId 变化时重新执行

  if (loading) return <p>加载中...</p>;
  if (!user) return <p>用户不存在</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Java 类比: useEffect ≈ @PostConstruct + 生命周期管理

// Java 类比
@Component
public class UserProfile {
    @PostConstruct          // ≈ useEffect(() => { ... }, [])
    public void init() {
        this.user = userService.findById(userId);
    }

    @PreDestroy             // ≈ useEffect 的返回清理函数
    public void cleanup() {
        // 清理资源
    }
}

useEffect 的三种模式:

// 模式 1:组件挂载时执行一次(依赖数组为空)
useEffect(() => {
  console.log('组件挂载了');
  // 类比 Java @PostConstruct
}, []);

// 模式 2:特定依赖变化时执行(依赖数组非空)
useEffect(() => {
  console.log(`userId 变成了 ${userId}`);
  fetchUser(userId);
}, [userId]);

// 模式 3:清理函数(组件卸载或依赖变化前执行)
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时任务执行');
  }, 1000);

  // 返回清理函数,组件卸载时自动调用
  // 类比 Java @PreDestroy
  return () => {
    clearInterval(timer);
    console.log('清理定时器');
  };
}, []);

4.3 useRef<T> — DOM 引用和值引用

useRef 有两种用途:引用 DOM 元素、保存不触发渲染的可变值。

import { useRef, useEffect } from 'react';

// 用途一:引用 DOM 元素
function TextInput() {
  // 初始值为 null,挂载后会被赋值为 input 元素
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    // 通过 ref 直接操作 DOM
    inputRef.current?.focus();
  };

  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current?.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="请输入..." />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}
// 用途二:保存不触发渲染的可变值(类似 Java 的实例变量)
function StopWatch() {
  const [elapsed, setElapsed] = useState(0);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (timerRef.current) return; // 防止重复启动
    timerRef.current = setInterval(() => {
      setElapsed(prev => prev + 1);
    }, 1000);
  };

  const stop = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

  // 组件卸载时清理
  useEffect(() => {
    return () => stop();
  }, []);

  return (
    <div>
      <p>已过去: {elapsed} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

Java 类比:

// useRef ≈ Java 的实例变量(不触发重新渲染)
public class StopWatch {
    private Timer timer;             // ≈ useRef<Timer>(null)
    private int elapsed = 0;         // ≈ useState(0)(这个会触发 UI 更新)
}

4.4 useMemo / useCallback — 性能优化

这两个 Hook 用于避免不必要的重复计算和函数重新创建。

import { useState, useMemo, useCallback } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');

  // useMemo:缓存计算结果,只在依赖变化时重新计算
  // 类比 Java 的 @Cacheable 或计算属性
  const filteredAndSorted = useMemo(() => {
    console.log('重新计算过滤和排序结果');
    return products
      .filter(p => p.name.includes(filter) || p.category.includes(filter))
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        return a.price - b.price;
      });
  }, [products, filter, sortBy]); // 只在这三个值变化时重新计算

  // useCallback:缓存函数引用,避免子组件不必要的重新渲染
  const handleDelete = useCallback((id: number) => {
    console.log('删除商品:', id);
    // 调用 API 删除...
  }, []); // 依赖为空,函数引用永远不变

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="搜索..."
      />
      <select value={sortBy} onChange={e => setSortBy(e.target.value as 'name' | 'price')}>
        <option value="name">按名称排序</option>
        <option value="price">按价格排序</option>
      </select>
      <p>共 {filteredAndSorted.length} 件商品</p>
      {filteredAndSorted.map(product => (
        <ProductCard key={product.id} product={product} onDelete={handleDelete} />
      ))}
    </div>
  );
}

// React.memo:当 Props 没变时跳过重新渲染(类似 Java 的 equals 比较)
const ProductCard = React.memo(function ProductCard({
  product,
  onDelete,
}: {
  product: Product;
  onDelete: (id: number) => void;
}) {
  console.log(`渲染商品: ${product.name}`);
  return (
    <div className="card">
      <h3>{product.name}</h3>
      <p>价格: ¥{product.price}</p>
      <button onClick={() => onDelete(product.id)}>删除</button>
    </div>
  );
});

何时使用 useMemo / useCallback:

场景 使用 不使用
昂贵的计算(排序、过滤大量数据) useMemo -
传给 memo 子组件的回调函数 useCallback -
简单的值计算 - 直接计算(优化反而增加开销)
组件内部使用的函数 - 直接定义(不影响子组件)

5. 自定义 Hook

自定义 Hook 是 React 中代码复用的主要方式,类似 Java 中的 Service 层。

基本规则

  1. 自定义 Hook 的函数名必须以 use 开头
  2. 内部可以调用其他 Hook(useState、useEffect 等)
  3. 每个组件调用 Hook 时都有自己独立的状态
// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

/**
 * 自定义 Hook:将状态持久化到 localStorage
 * Java 类比:带缓存的 ConfigService
 */
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  // 从 localStorage 读取初始值
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  // 值变化时写入 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(`写入 localStorage 失败 [${key}]:`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

export { useLocalStorage };
// 使用自定义 Hook
function ThemeToggle() {
  // 使用方式和 useState 完全一样,但值会持久化
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题: {theme}(点击切换)
    </button>
  );
}

实用自定义 Hook 示例

// src/hooks/useDebounce.ts
// 防抖 Hook:延迟更新值,避免频繁触发(如搜索输入)
import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

export { useDebounce };
// 使用防抖 Hook
function SearchBar() {
  const [input, setInput] = useState('');
  const debouncedSearch = useDebounce(input, 300); // 300ms 防抖

  useEffect(() => {
    if (debouncedSearch) {
      // 只在用户停止输入 300ms 后才发起搜索请求
      console.log('搜索:', debouncedSearch);
      fetchSearchResults(debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      value={input}
      onChange={e => setInput(e.target.value)}
      placeholder="搜索..."
    />
  );
}
// src/hooks/useToggle.ts
// 开关 Hook:管理 boolean 状态
import { useState, useCallback } from 'react';

function useToggle(initialValue: boolean = false): [boolean, () => void] {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(prev => !prev), []);
  return [value, toggle];
}

export { useToggle };
// 使用开关 Hook
function FAQ() {
  const [isOpen, toggleOpen] = useToggle(false);

  return (
    <div>
      <button onClick={toggleOpen}>
        {isOpen ? '收起' : '展开'} 详情
      </button>
      {isOpen && <p>这里是详细内容...</p>}
    </div>
  );
}

6. 事件处理类型

常用事件类型

import React from 'react';

function EventExamples() {
  // 输入框变化事件
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log('输入值:', e.target.value);
  };

  // 文本域变化事件
  const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    console.log('文本:', e.target.value);
  };

  // 下拉框变化事件
  const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    console.log('选中:', e.target.value);
  };

  // 表单提交事件
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // 阻止默认提交行为
    console.log('表单提交');
  };

  // 鼠标点击事件
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('点击坐标:', e.clientX, e.clientY);
  };

  // 键盘事件
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      console.log('按下回车');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleInputChange} onKeyDown={handleKeyDown} />
      <textarea onChange={handleTextareaChange} />
      <select onChange={handleSelectChange}>
        <option value="a">选项 A</option>
        <option value="b">选项 B</option>
      </select>
      <button type="submit" onClick={handleClick}>提交</button>
    </form>
  );
}

事件类型速查表

事件 类型 常用属性
onChange (input) React.ChangeEvent<HTMLInputElement> e.target.value
onChange (select) React.ChangeEvent<HTMLSelectElement> e.target.value
onSubmit (form) React.FormEvent<HTMLFormElement> e.preventDefault()
onClick React.MouseEvent<HTMLButtonElement> e.clientX, e.clientY
onKeyDown React.KeyboardEvent<HTMLInputElement> e.key, e.code
onFocus React.FocusEvent<HTMLInputElement> e.target
onDrag React.DragEvent<HTMLDivElement> e.dataTransfer

技巧: 如果不确定事件类型,可以先写内联处理函数,让 TypeScript 自动推断:

// 先写内联,让 IDE 告诉你类型
<input onChange={(e) => {
  // 鼠标悬停在 e 上,IDE 会显示 React.ChangeEvent<HTMLInputElement>
  console.log(e.target.value);
}} />

// 然后提取为独立函数
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

7. 条件渲染和列表渲染

条件渲染

interface StatusProps {
  isLoggedIn: boolean;
  user?: { name: string; role: string };
  error?: string;
}

function StatusDisplay({ isLoggedIn, user, error }: StatusProps) {
  // 方式一:三元表达式(适合简单的二选一)
  return (
    <div>
      {isLoggedIn ? <p>欢迎回来!</p> : <p>请登录</p>}
    </div>
  );
}

function DetailDisplay({ isLoggedIn, user, error }: StatusProps) {
  // 方式二:&& 短路运算(适合"有就显示")
  return (
    <div>
      {error && <p className="error">{error}</p>}
      {user && <p>用户名: {user.name}</p>}
    </div>
  );
}

function ComplexDisplay({ isLoggedIn, user, error }: StatusProps) {
  // 方式三:提前返回(适合复杂逻辑)
  if (error) {
    return <div className="error">错误: {error}</div>;
  }

  if (!isLoggedIn) {
    return <div>请登录后查看</div>;
  }

  return (
    <div>
      <h2>欢迎, {user?.name}</h2>
      <p>角色: {user?.role}</p>
    </div>
  );
}

Java 类比:

// Thymeleaf 条件渲染
// <div th:if="${isLoggedIn}">欢迎回来!</div>
// <div th:unless="${isLoggedIn}">请登录</div>

// React 的 {isLoggedIn ? ... : ...} 等价于 Thymeleaf 的 th:if/th:unless

列表渲染

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

function TodoList({ items }: { items: TodoItem[] }) {
  // 空列表处理
  if (items.length === 0) {
    return <p>暂无待办事项</p>;
  }

  return (
    <ul>
      {items.map(item => (
        // key 是 React 识别列表项的唯一标识,必须提供
        // 类比 Java: 类似 equals/hashCode 用于 diff 比较
        <li
          key={item.id}
          style={{ textDecoration: item.completed ? 'line-through' : 'none' }}
        >
          {item.text}
        </li>
      ))}
    </ul>
  );
}

关于 key 的规则:

// key 的正确用法
{items.map(item => (
  <li key={item.id}>{item.text}</li>     // 正确:用唯一 ID
))}

// key 的错误用法
{items.map((item, index) => (
  <li key={index}>{item.text}</li>        // 不推荐:用 index 作为 key
  // 当列表项顺序改变时,React 无法正确追踪
))}

完整组件示例

// 一个完整的待办列表组件,综合运用以上知识点
import { useState } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

  // 添加待办
  const addTodo = () => {
    if (!input.trim()) return;
    const newTodo: Todo = {
      id: Date.now(),
      text: input.trim(),
      completed: false,
    };
    setTodos(prev => [...prev, newTodo]);
    setInput('');
  };

  // 切换完成状态
  const toggleTodo = (id: number) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // 删除待办
  const deleteTodo = (id: number) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  // 过滤列表
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div>
      <h1>待办事项</h1>

      {/* 输入区域 */}
      <div>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="输入待办事项..."
        />
        <button onClick={addTodo}>添加</button>
      </div>

      {/* 过滤按钮 */}
      <div>
        {(['all', 'active', 'completed'] as const).map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            style={{ fontWeight: filter === f ? 'bold' : 'normal' }}
          >
            {f === 'all' ? '全部' : f === 'active' ? '未完成' : '已完成'}
          </button>
        ))}
      </div>

      {/* 列表 */}
      {filteredTodos.length === 0 ? (
        <p>暂无事项</p>
      ) : (
        <ul>
          {filteredTodos.map(todo => (
            <li key={todo.id}>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                {todo.text}
              </span>
              <button onClick={() => deleteTodo(todo.id)}>删除</button>
            </li>
          ))}
        </ul>
      )}

      {/* 统计 */}
      <p>共 {todos.length} 项,未完成 {todos.filter(t => !t.completed).length} 项</p>
    </div>
  );
}

export default TodoApp;

总结对照表

React 概念 Java 类比 说明
函数组件 类 + render 方法 接收 Props,返回 JSX
Props 构造函数参数 父组件传递的数据
children 组合模式/插槽 组件标签之间的内容
useState 实例变量 + setter(触发渲染) 组件的可变状态
useEffect @PostConstruct + @PreDestroy 副作用管理
useRef 实例变量(不触发渲染) DOM 引用或可变值
useMemo @Cacheable 缓存计算结果
useCallback 方法引用缓存 缓存函数引用
自定义 Hook Service 层 业务逻辑复用
key equals/hashCode 列表项唯一标识
JSX {} Thymeleaf ${} 在模板中嵌入表达式

动手练习

  1. 创建一个 Counter 组件,使用 useState 管理计数
  2. 创建一个 UserCard 组件,接收 Props 并显示用户信息
  3. 使用 useEffect 实现一个显示当前时间的组件(每秒更新)
  4. 封装一个 useWindowSize 自定义 Hook,监听窗口大小变化
  5. 实现一个简单的搜索过滤列表,综合使用 useState + 条件渲染 + 列表渲染