Skip to content

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>
  );
}

生命周期

类组件生命周期

  • 挂载阶段constructorcomponentWillMountrendercomponentDidMount
  • 更新阶段componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate
  • 卸载阶段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 zustand
tsx
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-redux
tsx
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-dom
tsx
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 jsdom
tsx
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();
  });
});

最佳实践

  1. 组件设计:保持组件职责单一
  2. TypeScript:使用类型定义提高代码可维护性
  3. Hooks 规则:只在顶层使用 Hooks,只在函数组件中调用 Hooks
  4. 状态管理:合理选择 local state、context 或外部状态管理
  5. 性能优化:必要时才进行优化,避免过早优化
  6. 代码组织:按功能或页面组织组件结构
  7. 错误处理:使用 Error Boundary 处理组件错误