JSX 与组件
函数组件
React 组件是返回 JSX 的 JavaScript 函数。组件名称必须以大写字母开头(小写 = HTML 标签)。Props 作为属性传递并在参数中解构。JSX 是 React.createElement() 的语法糖。始终返回单个根元素(或使用 Fragments)。组件必须是纯的——相同 props = 相同输出。
// Basic function component
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
<p>Welcome, {name}!</p>
);
// Composing components
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
<Greeting />
</div>
);
}JSX 表达式
JSX 允许在花括号 {} 中嵌入 JavaScript 表达式。可以放置变量、函数调用、三元运算符和任何返回值的表达式。语句(if、for、switch)不能直接使用——使用三元或 IIFE。布尔值(true、false)、null 和 undefined 不渲染任何内容。数字和字符串渲染为文本。对象不是有效的 React 子元素。
function Expression({ user, items }) {
const fullName = user.first + ' ' + user.last;
const itemCount = items.length;
return (
<div>
{/* Expressions in curly braces */}
<h1>{fullName}</h1>
<p>{itemCount} items</p>
{/* Conditional */}
<p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>
{/* Method calls */}
<p>{fullName.toUpperCase()}</p>
{/* Numbers and booleans render as nothing */}
<p>{false}{null}{undefined}</p>
</div>
);
}Fragments 与 JSX 列表
Fragments(<>...</>)将多个元素分组而不添加额外 DOM 节点——比包裹在 <div> 中更干净。需要传递 key 时使用 <Fragment key={...}>。JSX 元素数组需要唯一的 key 属性。虽然数组索引作为 key 对静态列表有效,但对动态列表使用稳定 ID 以防止渲染错误。Fragments 通过减少不必要的包裹元素提高性能。
// Fragment: group without extra DOM node
function App() {
return (
<>
<header>Header</header>
<main>Content</main>
<footer>Footer</footer>
</>
);
}
// Array of elements (needs keys)
function List() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{fruits.map((fruit, i) => (
<li key={i}>{fruit}</li>
))}
</ul>
);
}条件渲染
React 提供多种条件渲染模式。提前返回用于 if/else 逻辑。三元(cond ? A : B)用于二选一。逻辑与(cond && <Component/>)用于显示/隐藏。IIFE 用于复杂分支。避免在 JSX 中嵌入复杂逻辑——提取到变量或辅助函数。对于 switch 语句,使用查找对象或提取到单独函数。假值(0、'')会渲染,因此对数字使用三元而非 &&。
function Greeting({ isLoggedIn, user }) {
// 1. If/else (use early return)
if (!isLoggedIn) return <Login />;
// 2. Ternary operator
return (
<div>
{user ? <Dashboard user={user} /> : <Loading />}
{/* 3. Logical AND (render if truthy) */}
{user.isAdmin && <AdminPanel />}
{/* 4. IIFE for complex logic */}
{(() => {
if (user.role === 'admin') return <Admin />;
if (user.role === 'mod') return <Mod />;
return <User />;
})()}
</div>
);
}Children 与 Render Props
children prop 包含开标签和闭标签之间的元素——对于可组合组件(卡片、模态框、布局)至关重要。Render props 将函数作为 prop 传递,接收数据并返回 JSX——是 HOC 和 hooks 共享逻辑的替代方案。虽然 render props 在 hooks 出现后不太常见,但对于组件注入模式仍然有用。children 是不需要显式传递的特殊 prop。
// children prop: content between tags
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Profile">
<p>Name: Alice</p>
<p>Age: 30</p>
</Card>
// Render prop pattern
function DataProvider({ render }) {
const data = fetchData();
return <div>{render(data)}</div>;
}Props
传递 Props
Props 是从父组件传递到子组件的只读数据。可以是任何 JavaScript 值:字符串、数字、布尔值、数组、对象或函数。字符串值使用引号(name='Alice'),所有其他值使用花括号(age={30})。函数作为 props 实现子到父通信(回调)。Props 向下流动——子组件不能修改 props。对于双向数据绑定,将状态提升到共同父组件。
// Parent passes props to child
function App() {
return (
<User
name="Alice"
age={30}
isActive={true}
tags={['admin', 'dev']}
onClick={() => console.log('clicked')}
/>
);
}
// Child receives props
function User({ name, age, isActive, tags, onClick }) {
return (
<div onClick={onClick}>
<h1>{name}</h1>
<p>Age: {age}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}默认与可选 Props
默认 prop 值通过解构设置(param = defaultValue)。如果未传递 prop,则为 undefined。使用短路(bio && <p>)或三元来条件渲染可选 props。PropTypes(传统)或 TypeScript 接口可在开发时验证 prop 类型。defaultProps(类组件)对函数组件已弃用——使用解构默认值代替。
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
return (
<button className={'btn btn-' + color + ' btn-' + size}>
{children}
</button>
);
}
// Optional props (undefined if not passed)
function Profile({ name, bio }) {
return (
<div>
<h1>{name}</h1>
{bio && <p>{bio}</p>}
</div>
);
}
// Usage
<Button>Click</Button> {/* color='blue', size='md' */}
<Profile name="Alice" /> {/* bio is undefined */}展开与剩余 Props
展开运算符(...props)将所有 props 传递给子元素——对包裹组件(HOC、样式化组件)有用。剩余运算符在解构特定 props 后收集剩余 props。此模式在设计系统中很常见,包裹组件将未知 props 转发给 DOM 元素。注意:展开可能覆盖显式属性——顺序很重要({...props} className='x' vs className='x' {...props})。
// Spread: pass all props to child
function Input(props) {
return <input {...props} className="input" />;
}
// Usage
<Input type="text" placeholder="Name" value="Alice" />
// Rest: collect remaining props
function Button({ label, ...rest }) {
return <button {...rest}>{label}</button>;
}
// Selective spreading
function Card({ title, children, ...divProps }) {
return (
<div {...divProps}>
<h2>{title}</h2>
{children}
</div>
);
}Prop 类型与 TypeScript
TypeScript 接口为 props 提供编译时类型检查——新 React 项目的推荐方法。可选 props 使用 ?(isActive?: boolean)。PropTypes 提供运行时验证(仅在开发中),对没有 TypeScript 的 JavaScript 项目有用。isRequired 确保 prop 已提供。TypeScript 在运行前捕获类型错误,对大型代码库更优。
// TypeScript interface (recommended)
interface UserProps {
name: string;
age: number;
isActive?: boolean; // optional
onClick: (id: number) => void;
}
function User({ name, age, isActive = true, onClick }: UserProps) {
return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}
// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isActive: PropTypes.bool,
};Prop 逐层传递与 Context
Prop 逐层传递(prop drilling)发生在 props 通过不使用它们的多个组件层时。对于 2-3 层是可以接受的。对于更深的树,使用 Context API、状态管理库(Redux、Zustand)或组件组合。组合(将组件作为 props 或 children 传递)通常比 Context 更优雅地解决传递问题。问问自己:每个中间组件都需要这些数据吗?如果不需要,重新考虑组件结构。
// Prop drilling: passing through multiple levels
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserInfo user={user} />;
}
// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levelsuseState 与状态
基础 useState
useState 是向函数组件添加状态的基础 hook。它返回一个数组:[currentValue, setterFunction]。初始值可以是任何类型。调用 setter 触发使用新值的重新渲染。状态更新是异步的——调用 setCount 后值不会立即改变。每个组件实例都有自己的独立状态。setter 是稳定的(跨渲染保持相同引用)。
import { useState } from 'react';
function Counter() {
// [currentValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}函数式更新
当新状态依赖于前一个状态时,使用函数式更新:setCount(prev => prev + 1)。这保证使用最新状态,即使多个更新被批处理。没有函数式更新,快速连续调用可能使用过期状态。React 18 自动批处理状态更新(即使在 promise 和 timeout 中),因此函数式更新对正确性至关重要。
function Counter() {
const [count, setCount] = useState(0);
// BAD: may not work correctly with rapid updates
const incrementBad = () => setCount(count + 1);
// GOOD: functional update uses previous state
const incrementGood = () => setCount(prev => prev + 1);
// Batch updates
const addThree = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button onClick={addThree}>Count: {count}</button>;
}对象与数组状态
切勿直接修改状态——始终创建新对象/数组。对于对象,使用展开运算符复制现有属性:{...prev, [field]: value}。对于数组,使用展开添加([...prev, newItem])、filter 删除、map 更新。React 比较引用来检测变化——被修改的对象具有相同引用,因此 React 不会重新渲染。这是初学者 React bug 的头号来源。
function Form() {
const [form, setForm] = useState({ name: '', email: '', age: 0 });
// BAD: mutates state directly
// form.name = 'Alice'; setForm(form);
// GOOD: spread to create new object
const updateField = (field, value) => {
setForm(prev => ({ ...prev, [field]: value }));
};
return (
<input
value={form.name}
onChange={e => updateField('name', e.target.value)}
/>
);
}
// Array state
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}惰性初始状态
如果初始状态需要昂贵计算,向 useState 传递函数(惰性初始化)。该函数仅在首次渲染时运行,而非每次重新渲染。这对解析 localStorage、从 IndexedDB 获取或任何 CPU 密集型设置很重要。函数形式:useState(() => initialValue)。对于简单值(数字、字符串),直接传递值即可——惰性初始化不必要。
import { useState } from 'react';
function ExpensiveInit() {
// BAD: runs on every render (even though result is ignored)
const [data, setData] = useState(computeExpensiveValue());
// GOOD: lazy initialization — function runs only once
const [data2, setData2] = useState(() => computeExpensiveValue());
// Reading from localStorage
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('user');
return saved ? JSON.parse(saved) : null;
});
return <div>{data2}</div>;
}多个状态变量
对独立值使用多个 useState 调用而非一个大对象。这使更新更简单(无需展开)并防止不必要的重新渲染。将相关值分组到单个状态对象中(例如表单字段)。对于具有多个子值的复杂状态逻辑,考虑使用 useReducer。经验法则:如果状态更新独立,使用单独的 useState;如果相关/相互依赖,使用 useReducer 或单个对象。
function LoginForm() {
// Multiple independent state variables
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
setIsSubmitting(true);
// ... validation and submission
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password}
onChange={e => setPassword(e.target.value)} />
<button disabled={isSubmitting}>Submit</button>
</form>
);
}useEffect 与副作用
基础 useEffect
useEffect 在渲染后执行副作用。effect 函数在组件绘制后运行。清理函数(返回的)在下一次 effect 之前和卸载时运行——对清除计时器、订阅和监听器至关重要。依赖数组控制 effect 何时重新运行:[] = 挂载时一次,[dep] = dep 变化时,无数组 = 每次渲染。始终清理以防止内存泄漏。
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// Runs after every render
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function runs before next effect or unmount
return () => clearInterval(interval);
}, []); // empty array = run once on mount
return <p>Seconds: {seconds}</p>;
}依赖数组
依赖数组对 useEffect 行为至关重要。空数组 [] = 仅挂载(类似 componentDidMount)。带依赖 [a, b] = 挂载时和 a 或 b 变化时运行。无数组 = 每次渲染(很少是你想要的)。缺少依赖导致过期闭包。包含不必要依赖导致过度重新运行。使用 exhaustive-deps ESLint 规则捕获错误。effect 中使用的组件作用域的每个值都应在 deps 中。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Runs once on mount (empty deps)
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs when userId changes
useEffect(() => {
fetch('/api/users/' + userId)
.then(r => r.json())
.then(setUser);
}, [userId]); // re-run when userId changes
// Runs on every render (no deps) - rarely needed
useEffect(() => {
console.log('Every render');
});
return <div>{user?.name}</div>;
}清理与订阅
清 理对订阅、事件监听器、计时器和 WebSocket 连接至关重要。没有清理,会导致内存泄漏和重复处理程序。清理函数运行:(1) 在下一次 effect 重新运行之前,(2) 组件卸载时。对于 WebSocket/事件监听器,始终在清理中移除它们。对于依赖于 effect 的状态,在清理中重置以避免显示前一个 roomId 的过期数据。
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com/' + roomId);
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// Cleanup: close connection when roomId changes or unmount
return () => {
ws.close();
setMessages([]); // reset for new room
};
}, [roomId]);
// Window event listener
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{messages.length} messages</div>;
}获取数据
useEffect 中的数据获取需要取消标志以防止卸载后设置状态(导致 React 警告)。'cancelled' 标志确保 setUsers/setError/setLoading 仅在组件仍挂载时运行。对于生产应用,考虑使用数据获取库(React Query、SWR),它们自动处理缓存、去重和竞态条件。空依赖数组 [] 确保获取仅在挂载时发生一次。
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const res = await fetch('/api/users');
const data = await res.json();
if (!cancelled) setUsers(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
};
fetchData();
return () => { cancelled = true; };
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}useLayoutEffect 与 useEffect
useEffect 在浏览器绘制后异步运行——如果测量 DOM 元素,用户可能看到短暂闪烁。useLayoutEffect 在 DOM 变更后但在绘制前同步运行——防止视觉闪烁。对于影响布局的 DOM 测量(getBoundingClientRect、滚动位置)使用 useLayoutEffect。其他所有情况使用 useEffect(它不阻塞绘制)。在服务器上,useLayoutEffect 会警告——使用 useIsomorphicLayoutEffect 模式。
import { useState, useEffect, useLayoutEffect } from 'react';
function Tooltip({ text }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// useEffect: runs AFTER paint (user may see flash)
useEffect(() => {
const rect = document.getElementById('tip').getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, [text]);
// useLayoutEffect: runs BEFORE paint (no flash)
useLayoutEffect(() => {
const rect = document.getElementById('tip').getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, [text]);
return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}useRef、useMemo 与 useCallback
useRef 基础
useRef 返回一个跨渲染持久化的可变对象 { current: value }。与状态不同,更改 ref.current 不会触发重新渲染。常见用途:(1) 访问 DOM 元素(通过 ref 属性),(2) 存储不影响渲染的可变值(计时器、前一个值),(3) 存储最新值供回调使用。ref 对象跨渲染具有相同标识。初始值传递给 useRef(initialValue)。
import { useRef } from 'react';
function FocusInput() {
// ref to access DOM element
const inputRef = useRef(null);
const focus = () => inputRef.current.focus();
const clear = () => {
inputRef.current.value = '';
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus</button>
<button onClick={clear}>Clear</button>
</div>
);
}useRef 用于可变值
useRef 存储跨渲染持久化的可变值而不触发重新渲染。这非常适合:计时器 ID、WebSocket 引用、跟踪前一个状态和计数渲染次数。由于更改 ref.current 不会导致重新渲染,更改它时 UI 不会更新——对于应影响 UI 的值使用状态。渲染计数模式(ref.current++)对调试有用但不应在生产逻辑中使用。
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const renderCount = useRef(0);
// Track render count (doesn't trigger re-render)
renderCount.current++;
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<div>
<p>{seconds}s (render #{renderCount.current})</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useMemo
useMemo 记忆化(缓存)计算值,仅在依赖变化时重新计算。用于昂贵计算(过滤、排序、复杂数学)以避免每次渲染都重新运行。依赖数组的工作方式类似 useEffect。过度使用可能损害性能(记忆化有其自身开销)——仅记忆化真正昂贵的操作。useMemo 也用于保留对象引用以防止子组件重新渲染。
import { useState, useMemo } from 'react';
function ProductList({ products, filter }) {
const [search, setSearch] = useState('');
// Memoize expensive computation
const filtered = useMemo(() => {
console.log('Filtering...');
return products
.filter(p => p.category === filter)
.filter(p => p.name.includes(search));
}, [products, filter, search]); // recompute only when these change
// Memoize a value
const totalPrice = useMemo(() =>
filtered.reduce((sum, p) => sum + p.price, 0),
[filtered]);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<p>Total: ${totalPrice}</p>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}useCallback
useCallback 记忆化函数,除非依赖变化否则跨渲染返回相同引用。这防止记忆化子组件(包裹在 memo() 中)的不必要重新渲染。没有 useCallback,每次父组件渲染都创建新函数引用,导致 memo() 子组件重新渲染。将回调传递给优化的子组件时使用 useCallback。与 useMemo 一样,不要过度使用——仅用于作为 props 传递给记忆化子组件的函数。
import { useState, useCallback, memo } from 'react';
// Memoized child component
const Button = memo(function Button({ onClick, label }) {
console.log('Button rendered');
return <button onClick={onClick}>{label}</button>;
});
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Without useCallback: new function every render
// const handleClick = () => setCount(c => c + 1);
// With useCallback: stable function reference
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // empty deps = stable forever
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<Button onClick={handleClick} label="Click" />
<p>Count: {count}</p>
</div>
);
}转发 Refs
forwardRef 允许父组件将 ref 传递给子组件的 DOM 元素。useImperativeHandle 自定义 ref 暴露的内容——而非 DOM 节点,可以暴露特定方法(focus、clear、getValue)。这对于创建具有命令式 API 的可重用输入组件很有用。React 19 简化了 refs(ref 现在是常规 prop),但库仍需要 forwardRef。避免过度使用命令式句柄——优先使用声明式 props。
import { useRef, forwardRef, useImperativeHandle } from 'react';
// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} />;
});
// Usage
function App() {
const ref = useRef(null);
return (
<>
<CustomInput ref={ref} />
<button onClick={() => ref.current.focus()}>Focus</button>
<button onClick={() => ref.current.clear()}>Clear</button>
</>
);
}useReducer 与 Context
useReducer 基础
useReducer 是复杂状态逻辑的 useState 替代方案。reducer 是纯函数:(state, action) => newState。Action 描述发生了什么;reducer 决定如何更新状态。此模式使状态转换可预测且可测试。dispatch 是稳定的(相同引用)。始终返回新状态对象(切勿修改)。默认情况应为未知 action 抛出错误。当状态有多个子值或下一状态依赖复杂逻辑时使用 useReducer。