第六部分 - 第 2 节 | 面向 Java 工程师的 React 入门
React 是 Facebook(Meta)开发的前端 UI 库,核心思想是用组件化的方式构建用户界面。
Java 工程师的理解方式:
Java 后端渲染(你熟悉的):
Controller 接收请求 → 准备数据 → 模板引擎(Thymeleaf)渲染 HTML → 返回给浏览器
React 前端渲染(新方式):
浏览器加载 JS → React 组件根据数据生成虚拟 DOM → 渲染到页面
数据变化时 → React 自动计算差异 → 只更新变化的部分
<!-- 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) |
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!');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 表达式,不是模板语言。
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>";
}
}// 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 是 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[]; // 数组Hooks 是 React 16.8 引入的特性,让函数组件可以拥有状态和副作用。
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>
);
}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('清理定时器');
};
}, []);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 更新)
}这两个 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 | - |
| 简单的值计算 | - | 直接计算(优化反而增加开销) |
| 组件内部使用的函数 | - | 直接定义(不影响子组件) |
自定义 Hook 是 React 中代码复用的主要方式,类似 Java 中的 Service 层。
- 自定义 Hook 的函数名必须以
use开头 - 内部可以调用其他 Hook(useState、useEffect 等)
- 每个组件调用 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>
);
}// 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>
);
}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);
};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:unlessinterface 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 ${} |
在模板中嵌入表达式 |
- 创建一个
Counter组件,使用useState管理计数 - 创建一个
UserCard组件,接收Props并显示用户信息 - 使用
useEffect实现一个显示当前时间的组件(每秒更新) - 封装一个
useWindowSize自定义 Hook,监听窗口大小变化 - 实现一个简单的搜索过滤列表,综合使用 useState + 条件渲染 + 列表渲染