Appearance
React
The library for web and native user interfaces
官方网站
简介
React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发并开源。它采用组件化开发模式,通过声明式语法高效地更新和渲染 UI。React 的核心思想是"一次学习,随处编写"。
核心概念
1. JSX
JSX 是 JavaScript 的语法扩展,允许在 JavaScript 中编写类似 HTML 的结构。
tsx
// 函数组件
function Welcome({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
// 类组件
class Welcome extends React.Component<{ name: string }, {}> {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}2. 组件
组件是 React 应用的基本构建块,分为函数组件和类组件。
tsx
// 简单按钮组件
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}
function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
const baseClass = 'px-4 py-2 rounded';
const variantClass = variant === 'primary'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-800';
return (
<button
className={`${baseClass} ${variantClass}`}
onClick={onClick}
>
{children}
</button>
);
}3. Props
Props 是父组件传递给子组件的数据,只读不可变。
tsx
// 父组件
function App() {
const user = { name: 'Alice', age: 30 };
return <UserCard name={user.name} age={user.age} />;
}
// 子组件
interface UserCardProps {
name: string;
age: number;
}
function UserCard({ name, age }: UserCardProps) {
return (
<div className="card">
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
}4. State
State 是组件内部管理的可变数据。
tsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}5. Hooks
Hooks 是 React 16.8 引入的特性,允许在函数组件中使用 state 和其他 React 特性。
useState
tsx
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([
...todos,
{ id: Date.now(), text: inputValue, completed: false }
]);
setInputValue('');
}
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}useEffect
tsx
import { useEffect, useState } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data.slice(0, 10));
} catch (error) {
console.error('Failed to fetch posts:', error);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}useContext
tsx
import { createContext, useContext, useState } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)!;
return (
<button
onClick={toggleTheme}
className={theme === 'dark' ? 'dark-mode' : ''}
>
Toggle Theme
</button>
);
}useRef
tsx
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" placeholder="Auto-focused input" />;
}useMemo 和 useCallback
tsx
import { useMemo, useCallback, useState } from 'react';
function ExpensiveComponent({ data, filter }: { data: number[]; filter: string }) {
// 使用 useMemo 缓存计算结果
const filteredData = useMemo(() => {
return data.filter(item =>
item.toString().includes(filter)
);
}, [data, filter]);
// 使用 useCallback 缓存函数引用
const handleClick = useCallback((id: number) => {
console.log('Clicked:', id);
}, []);
return (
<ul>
{filteredData.map(item => (
<li key={item} onClick={() => handleClick(item)}>
{item}
</li>
))}
</ul>
);
}生命周期
类组件生命周期
- 挂载阶段:
constructor→componentWillMount→render→componentDidMount - 更新阶段:
componentWillReceiveProps→shouldComponentUpdate→componentWillUpdate→render→componentDidUpdate - 卸载阶段:
componentWillUnmount
函数组件等效方案
tsx
import { useEffect, useLayoutEffect } from 'react';
function LifecycleComponent() {
// componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('Component mounted or updated');
// componentWillUnmount
return () => {
console.log('Component will unmount');
};
}, []); // 空依赖数组表示仅挂载时执行
// 同步执行,类似 componentWillMount/componentWillUpdate
useLayoutEffect(() => {
console.log('Synchronous effect');
}, []);
return <div>Component</div>;
}事件处理
tsx
import { useState, FormEvent } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log('Login:', { username, password });
};
const handleChange = (setter: (value: string) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setter(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={handleChange(setUsername)}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={handleChange(setPassword)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}条件渲染
tsx
function UserProfile({ user }: { user: { name: string } | null }) {
// 方式一:条件运算符
return (
<div>
{user ? (
<h1>Welcome, {user.name}</h1>
) : (
<h1>Please login</h1>
)}
</div>
);
}
// 方式二:逻辑与运算符
function Notification({ count }: { count: number }) {
return (
<div>
<h1>Hello</h1>
{count > 0 && <p>You have {count} messages</p>}
</div>
);
}
// 方式三:三元运算符简化
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return isLoggedIn ? <UserCard /> : <LoginButton />;
}列表渲染
tsx
function TodoList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
interface Product {
id: string;
name: string;
price: number;
}
function ProductList({ products }: { products: Product[] }) {
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>${product.price.toFixed(2)}</p>
</div>
))}
</div>
);
}状态管理
1. useReducer
tsx
import { useReducer } from 'react';
interface State {
count: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}2. Zustand
bash
npm install zustandtsx
import { create } from 'zustand';
interface BearState {
bears: number;
increaseBears: () => void;
removeBears: () => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
removeBears: () => set({ bears: 0 }),
}));
function BearCounter() {
const bears = useBearStore((state) => state.bears);
const increaseBears = useBearStore((state) => state.increaseBears);
return (
<div>
<h1>{bears} bears</h1>
<button onClick={increaseBears}>Add bear</button>
</div>
);
}3. Redux Toolkit
bash
npm install @reduxjs/toolkit react-reduxtsx
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(counterSlice.actions.increment())}>
+
</button>
<button onClick={() => dispatch(counterSlice.actions.decrement())}>
-
</button>
</div>
);
}路由
React Router
bash
npm install react-router-domtsx
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
function Home() {
return <h1>Home Page</h1>;
}
function UserProfile() {
const { id } = useParams();
const navigate = useNavigate();
return (
<div>
<h1>User Profile: {id}</h1>
<button onClick={() => navigate('/')}>Back to Home</button>
</div>
);
}
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/user/123">User 123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserProfile />} />
</Routes>
</BrowserRouter>
);
}性能优化
1. React.memo
tsx
const ExpensiveList = memo(function ExpensiveList({ items }: { items: number[] }) {
return (
<ul>
{items.map((item, i) => (
<li key={i}>{expensiveCalculation(item)}</li>
))}
</ul>
);
});2. useCallback 和 useMemo
tsx
function Parent() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState<string[]>([]);
const addTodo = useCallback(() => {
setTodos(prev => [...prev, 'New todo']);
}, []);
const expensiveValue = useMemo(() => {
return heavyComputation(count);
}, [count]);
return (
<div>
<Child onAdd={addTodo} />
<ExpensiveComponent value={expensiveValue} />
</div>
);
}3. 代码分割
tsx
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}测试
Vitest + React Testing Library
bash
npm install -D vitest @testing-library/react @testing-library/user-event jsdomtsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('renders counter', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
it('increments counter', () => {
render(<Counter />);
fireEvent.click(screen.getByText('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('decrements counter', () => {
render(<Counter />);
fireEvent.click(screen.getByText('Decrement'));
expect(screen.getByText('Count: -1')).toBeInTheDocument();
});
});最佳实践
- 组件设计:保持组件职责单一
- TypeScript:使用类型定义提高代码可维护性
- Hooks 规则:只在顶层使用 Hooks,只在函数组件中调用 Hooks
- 状态管理:合理选择 local state、context 或外部状态管理
- 性能优化:必要时才进行优化,避免过早优化
- 代码组织:按功能或页面组织组件结构
- 错误处理:使用 Error Boundary 处理组件错误