React Hooks 是 React 16.8 引入的革命性特性,它让你在函数组件中使用状态(state)和其他 React 特性,而无需编写 class。

简单来说:Hooks 就是让函数组件拥有类组件的能力,但代码更简洁。


一、为什么需要 Hooks?

1.1 类组件的痛点

// 类组件示例 - 复杂、冗余
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this); // 烦人的绑定
  }

  componentDidMount() {
    // 初始化逻辑
    console.log('组件挂载');
  }

  componentDidUpdate() {
    // 更新逻辑
    console.log('组件更新');
  }

  componentWillUnmount() {
    // 清理逻辑
    console.log('组件卸载');
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>你点击了 {this.state.count} </p>
        <button onClick={this.handleClick}>
          点击我
        </button>
      </div>
    );
  }
}

类组件的问题:

  • this 绑定问题:需要手动绑定或使用箭头函数
  • 逻辑分散:相关代码分散在多个生命周期方法中
  • 状态逻辑复用难:需要使用高阶组件或 Render Props
  • 组件嵌套地狱:多个高阶组件嵌套导致代码难以理解

1.2 Hooks 的优势

// 使用 Hooks 的函数组件 - 简洁、直观
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('组件挂载或更新');
    return () => {
      console.log('清理副作用');
    };
  }, [count]);

  return (
    <div>
      <p>你点击了 {count} </p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

二、核心 Hooks 详解

2.1 useState - 状态管理

作用:为函数组件添加状态

import { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量,初始值为 0
  const [count, setCount] = useState(0);
  // 可以声明多个 state 变量
  const [name, setName] = useState('张三');
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' },
    { id: 2, text: '学习 Hooks' }
  ]);

  return (
    <div>
      <p>姓名: {name}</p>
      <p>点击次数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
      <button onClick={() => setName('李四')}>
        改名
      </button>
      {/* 更新数组 */}
      <button onClick={() => {
        setTodos([...todos, { id: 3, text: '学习 TypeScript' }]);
      }}>
        添加待办
      </button>
    </div>
  );
}

2.2 useEffect - 副作用处理

  • 作用:处理副作用(数据获取、订阅、手动修改 DOM 等).
  • 什么是“副作用”: 在 React 组件中,主要职责是计算和渲染 UI。但除了渲染 UI,我们常常还需要做一些其他事情,比如:
    • 数据获取:从 API 获取数据.
    • 订阅:设置一个 WebSocket 连接或 addEventListener.
    • 手动修改 DOM:直接操作页面标题、焦点等.
    • 设置定时器:setTimeout 或 setInterval.

这些在组件渲染之外,与外部系统进行交互的操作,就叫做副作用.

2.2.1 useEffect 示例

package.json
index.html
index.js
App.jsx
App.css
{
  "name": "react",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build --base ./"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.2.1",
    "vite": "^5.0.8"
  }
}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Hello World</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index.jsx"></script>
  </body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
import React, { useState, useEffect } from 'react';
import './App.css';

function TodoApp() {
  // 状态
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
  const [countdown, setCountdown] = useState(5); // 倒计时(秒)

  // 🔹 useEffect 1:挂载时从 localStorage 加载数据
  useEffect(() => {
    console.log('✅ 挂载:尝试从 localStorage 加载待办事项...');
    const saved = localStorage.getItem('react-todos');
    if (saved) {
      try {
        setTodos(JSON.parse(saved));
      } catch (e) {
        console.error('❌ 解析 localStorage 数据失败', e);
      }
    }
  }, []); // 仅挂载时执行

  // 🔹 useEffect 2:当 todos 变化时,自动保存到 localStorage(持久化)
  useEffect(() => {
    console.log('💾 自动保存 todos 到 localStorage');
    localStorage.setItem('react-todos', JSON.stringify(todos));
  }, [todos]); // 依赖 todos

  // 🔹 useEffect 3:根据 filter 和 todos 更新页面标题
  useEffect(() => {
    const activeCount = todos.filter(t => !t.completed).length;
    const title = filter === 'completed'
      ? `✅ ${todos.filter(t => t.completed).length} 已完成`
      : filter === 'active'
        ? `⏳ ${activeCount} 进行中`
        : `📝 全部 (${todos.length})`;

    document.title = `Todo App - ${title}`;
  }, [todos, filter]); // 依赖 todos 和 filter

  // 🔹 useEffect 4:设置倒计时提醒(每秒减1),卸载时清除
  useEffect(() => {
    console.log('⏰ 启动倒计时提醒...');
    const timer = setInterval(() => {
      setCountdown(prev => {
        if (prev <= 1) {
          // 倒计时结束,重置为5
          return 5;
        }
        return prev - 1;
      });
    }, 1000);

    // 🔁 清理函数:组件卸载或重新执行前清除定时器
    return () => {
      console.log('🧹 清理倒计时定时器');
      clearInterval(timer);
    };
  }, []); // 仅挂载时设置

  // 事件处理
  const addTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim() === '') return;
    
    const newTodo = {
      id: Date.now(),
      text: inputValue.trim(),
      completed: false,
      createdAt: new Date().toISOString()
    };
    
    setTodos([...todos, newTodo]);
    setInputValue('');
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 过滤后的待办事项
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div className="app">
      <header className="header">
        <h1>📝 React Todo List</h1>
        <div className="countdown-badge">
          提醒倒计时: <strong>{countdown}s</strong>
        </div>
      </header>

      {/* 添加表单 */}
      <form onSubmit={addTodo} className="add-form">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入新任务,回车添加..."
          className="input"
          autoFocus
        />
        <button type="submit" className="add-btn">+</button>
      </form>

      {/* 过滤器 */}
      <div className="filters">
        <button 
          onClick={() => setFilter('all')} 
          className={filter === 'all' ? 'active' : ''}
        >
          全部 ({todos.length})
        </button>
        <button 
          onClick={() => setFilter('active')} 
          className={filter === 'active' ? 'active' : ''}
        >
          进行中 ({todos.filter(t => !t.completed).length})
        </button>
        <button 
          onClick={() => setFilter('completed')} 
          className={filter === 'completed' ? 'active' : ''}
        >
          已完成 ({todos.filter(t => t.completed).length})
        </button>
      </div>

      {/* 待办列表 */}
      <ul className="todo-list">
        {filteredTodos.length === 0 ? (
          <li className="empty">暂无任务 {filter === 'completed' ? '✅' : '⏳'}</li>
        ) : (
          filteredTodos.map(todo => (
            <li key={todo.id} className={todo.completed ? 'completed' : ''}>
              <label>
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onChange={() => toggleTodo(todo.id)}
                />
                <span className="todo-text">{todo.text}</span>
              </label>
              <button 
                onClick={() => deleteTodo(todo.id)} 
                className="delete-btn"
                aria-label="删除"
              >
                ×
              </button>
            </li>
          ))
        )}
      </ul>

      {/* 底部操作 */}
      {todos.length > 0 && (
        <div className="footer">
          <span>
            {todos.filter(t => !t.completed).length} 项未完成
          </span>
          <button onClick={clearCompleted} className="clear-btn">
            清除已完成
          </button>
        </div>
      )}

      <footer className="credits">
        💡 数据自动保存至浏览器 localStorage | 刷新页面不丢失
      </footer>
    </div>
  );
}

export default TodoApp;
.app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.header {
  text-align: center;
  margin-bottom: 20px;
  position: relative;
}

.countdown-badge {
  position: absolute;
  top: 0;
  right: 0;
  background: #ff6b6b;
  color: white;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 0.85rem;
}

.add-form {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}

.input {
  flex: 1;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 6px;
  font-size: 1rem;
}

.input:focus {
  outline: none;
  border-color: #4caf50;
}

.add-btn {
  width: 40px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 1.5rem;
  cursor: pointer;
}

.filters {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.filters button {
  flex: 1;
  min-width: 80px;
  padding: 8px;
  border: 1px solid #ccc;
  background: #f8f9fa;
  border-radius: 4px;
  cursor: pointer;
}

.filters button.active {
  background: #4caf50;
  color: white;
  border-color: #4caf50;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed .todo-text {
  text-decoration: line-through;
  color: #888;
}

.todo-list li:hover {
  background-color: #f9f9f9;
}

.todo-text {
  margin-left: 8px;
}

.delete-btn {
  background: none;
  border: none;
  color: #ff6b6b;
  font-size: 1.2rem;
  cursor: pointer;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.empty {
  text-align: center;
  color: #888;
  padding: 20px;
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid #eee;
}

.clear-btn {
  background: #ff6b6b;
  color: white;
  border: none;
  padding: 6px 12px;
  border-radius: 4px;
  cursor: pointer;
}

.credits {
  text-align: center;
  margin-top: 30px;
  color: #888;
  font-size: 0.85rem;
}
+ 在线测试

2.3 useContext - 跨组件传值

作用:useContext 提供了一种在组件树中跨层级传递数据的方法,而无需通过每一层组件手动地传递 props。简单来说,它解决了 React 中一个常见的痛点:Props 穿透

2.3.1 不使用 useContext 的痛点

<App>
  <Page>
    <Layout>
      <Header> {/* Header 需要主题信息 */}
        <ThemeButton> {/* ThemeButton 也需要主题信息来改变样式 */}
          点击我
        </ThemeButton>
      </Header>
    </Layout>
  </Page>
</App>

如果 App 组件有一个 theme 状态,ThemeButton 需要用它来决定自己的样式。在 useContext 出现之前,你必须这样传递数据:

  1. App 把 theme 传给 Page。
  2. Page 对 theme 没兴趣,但只能把它再传给 Layout.
  3. Layout 也对 theme 没兴趣,也只能把它传给 Header.
  4. Header 可能还是用不上,最后才传给 ThemeButton.

这个过程就像一个“击鼓传花”的游戏,中间的组件被迫接收它们自己并不需要的 props,这使得代码变得冗余、难以维护.

2.3.2 使用 usecontext 示例

package.json
index.html
index.js
App.jsx
App.css
{
  "name": "react",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build --base ./"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.2.1",
    "vite": "^5.0.8"
  }
}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Hello World</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index.jsx"></script>
  </body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
// App.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import './App.css';

// 🔹 1. 创建 Context(主题 + 语言)
const AppContext = createContext();

// 🔹 2. 自定义 Hook:useAppContext(推荐写法)
const useAppContext = () => {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }
  return context;
};

// 🔹 3. Provider 组件:包裹整个应用,提供状态和方法
const AppProvider = ({ children }) => {
  // 从 localStorage 读取初始值(持久化)
  const getInitialTheme = () => {
    const saved = localStorage.getItem('app-theme');
    return saved === 'dark' ? 'dark' : 'light';
  };

  const getInitialLang = () => {
    const saved = localStorage.getItem('app-lang');
    return saved === 'en' ? 'en' : 'zh';
  };

  const [theme, setTheme] = useState(getInitialTheme());
  const [lang, setLang] = useState(getInitialLang());

  // 🔸 useEffect:当 theme/lang 变化时,保存到 localStorage
  useEffect(() => {
    localStorage.setItem('app-theme', theme);
    // 更新 <html> class,方便全局样式控制
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);

  useEffect(() => {
    localStorage.setItem('app-lang', lang);
  }, [lang]);

  // 切换主题
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  // 切换语言
  const toggleLanguage = () => {
    setLang(prev => prev === 'zh' ? 'en' : 'zh');
  };

  // 翻译函数(简单实现,实际可用 i18n 库)
  const t = (key) => {
    const translations = {
      zh: {
        dashboard: '仪表盘',
        welcome: name => `欢迎回来,${name}!`,
        profile: '个人资料',
        settings: '设置',
        logout: '退出登录',
        theme: '主题',
        language: '语言',
        light: '亮色',
        dark: '暗色',
        chinese: '中文',
        english: 'English',
        tasks: '待办事项',
        completed: '已完成',
        pending: '进行中',
        notifications: '通知',
        newMessage: '你有 3 条新消息',
        systemUpdate: '系统将在 5 分钟后更新',
        viewAll: '查看全部',
        noTasks: '暂无待办事项'
      },
      en: {
        dashboard: 'Dashboard',
        welcome: name => `Welcome back, ${name}!`,
        profile: 'Profile',
        settings: 'Settings',
        logout: 'Logout',
        theme: 'Theme',
        language: 'Language',
        light: 'Light',
        dark: 'Dark',
        chinese: '中文',
        english: 'English',
        tasks: 'Tasks',
        completed: 'Completed',
        pending: 'Pending',
        notifications: 'Notifications',
        newMessage: 'You have 3 new messages',
        systemUpdate: 'System will update in 5 minutes',
        viewAll: 'View All',
        noTasks: 'No tasks yet'
      }
    };
    return translations[lang][key] || key;
  };

  return (
    <AppContext.Provider value={{
      theme,
      lang,
      toggleTheme,
      toggleLanguage,
      t
    }}>
      {children}
    </AppContext.Provider>
  );
};

// 🔹 4. 子组件:Header(使用 Context)
const Header = () => {
  const { theme, lang, toggleTheme, toggleLanguage, t } = useAppContext();
  
  return (
    <header className="header">
      <h1>{t('dashboard')}</h1>
      <div className="controls">
        <button 
          onClick={toggleLanguage}
          className="control-btn"
          title={t('language')}
        >
          {lang === 'zh' ? '🇨🇳' : '🇺🇸'}
        </button>
        <button 
          onClick={toggleTheme}
          className="control-btn"
          title={t('theme')}
        >
          {theme === 'light' ? '🌙' : '☀️'}
        </button>
        <button className="control-btn logout-btn" title={t('logout')}>
          👤
        </button>
      </div>
    </header>
  );
};

// 🔹 5. 子组件:Sidebar(使用 Context)
const Sidebar = () => {
  const { t } = useAppContext();
  
  const menuItems = [
    { icon: '📊', label: t('dashboard'), active: true },
    { icon: '👤', label: t('profile') },
    { icon: '⚙️', label: t('settings') }
  ];

  return (
    <aside className="sidebar">
      <nav>
        <ul>
          {menuItems.map((item, index) => (
            <li key={index} className={item.active ? 'active' : ''}>
              <span>{item.icon}</span>
              <span>{item.label}</span>
            </li>
          ))}
        </ul>
      </nav>
    </aside>
  );
};

// 🔹 6. 子组件:TaskCard(使用 Context)
const TaskCard = () => {
  const { t } = useAppContext();
  
  const tasks = [
    { id: 1, title: '完成项目提案', status: 'pending' },
    { id: 2, title: '审核设计稿', status: 'completed' },
    { id: 3, title: '团队周会', status: 'pending' }
  ];

  return (
    <div className="card">
      <div className="card-header">
        <h2>{t('tasks')}</h2>
        <span>{tasks.filter(t => t.status === 'pending').length} {t('pending')}</span>
      </div>
      <ul className="task-list">
        {tasks.length > 0 ? (
          tasks.map(task => (
            <li key={task.id} className={task.status}>
              <label>
                <input 
                  type="checkbox" 
                  defaultChecked={task.status === 'completed'} 
                />
                <span>{task.title}</span>
              </label>
            </li>
          ))
        ) : (
          <li className="empty">{t('noTasks')}</li>
        )}
      </ul>
    </div>
  );
};

// 🔹 7. 子组件:NotificationCard(使用 Context)
const NotificationCard = () => {
  const { t } = useAppContext();
  
  return (
    <div className="card">
      <div className="card-header">
        <h2>{t('notifications')}</h2>
        <button className="view-all">{t('viewAll')}</button>
      </div>
      <div className="notification-item">
        <span className="dot"></span>
        <p>{t('newMessage')}</p>
      </div>
      <div className="notification-item">
        <span className="dot warning"></span>
        <p>{t('systemUpdate')}</p>
      </div>
    </div>
  );
};

// 🔹 8. 主应用组件
const Dashboard = () => {
  const { t } = useAppContext();
  
  return (
    <div className="dashboard">
      <Header />
      <div className="main-content">
        <Sidebar />
        <main className="content">
          <div className="welcome">
            <h2>{t('welcome')('张三')}</h2>
            <p>2024年6月15日  周六</p>
          </div>
          
          <div className="cards-grid">
            <TaskCard />
            <NotificationCard />
          </div>
        </main>
      </div>
    </div>
  );
};

// 🔹 9. 根组件:包裹 Provider
function App() {
  return (
    <AppProvider>
      <Dashboard />
    </AppProvider>
  );
}

export default App;
/* App.css */
:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f8f9fa;
  --text-primary: #212529;
  --text-secondary: #6c757d;
  --border-color: #e9ecef;
  --card-bg: #ffffff;
  --sidebar-bg: #ffffff;
  --accent: #4361ee;
  --success: #4cc9f0;
  --warning: #f72585;
}

[data-theme="dark"] {
  --bg-primary: #121212;
  --bg-secondary: #1e1e1e;
  --text-primary: #e0e0e0;
  --text-secondary: #a0a0a0;
  --border-color: #333333;
  --card-bg: #1e1e1e;
  --sidebar-bg: #1a1a1a;
  --accent: #4cc9f0;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
  font-family: 'Segoe UI', system-ui, sans-serif;
  transition: background-color 0.3s, color 0.3s;
}

.dashboard {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border-color);
}

.header h1 {
  font-size: 1.8rem;
  font-weight: 700;
}

.controls {
  display: flex;
  gap: 0.5rem;
}

.control-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: var(--card-bg);
  color: var(--text-primary);
  font-size: 1.2rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.control-btn:hover {
  transform: scale(1.1);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.logout-btn {
  background: #ff6b6b !important;
  color: white !important;
}

.main-content {
  display: flex;
  flex: 1;
}

.sidebar {
  width: 220px;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border-color);
  padding: 1.5rem 0;
}

.sidebar ul {
  list-style: none;
}

.sidebar li {
  padding: 0.8rem 1.5rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  transition: background 0.2s;
}

.sidebar li:hover,
.sidebar li.active {
  background: var(--accent);
  color: white;
}

.sidebar li.active {
  border-left: 4px solid #fff;
}

.content {
  flex: 1;
  padding: 2rem;
  overflow-y: auto;
}

.welcome h2 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

.welcome p {
  color: var(--text-secondary);
  font-size: 1.1rem;
}

.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.5rem;
  margin-top: 2rem;
}

.card {
  background: var(--card-bg);
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
  overflow: hidden;
  transition: box-shadow 0.3s;
}

[data-theme="dark"] .card {
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}

.card:hover {
  box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.2rem 1.5rem;
  border-bottom: 1px solid var(--border-color);
}

.card-header h2 {
  font-size: 1.3rem;
  font-weight: 600;
}

.view-all {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-weight: 500;
}

.task-list {
  list-style: none;
  padding: 0.5rem 0;
}

.task-list li {
  padding: 0.8rem 1.5rem;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
}

.task-list li:last-child {
  border-bottom: none;
}

.task-list li.pending input {
  accent-color: var(--accent);
}

.task-list li.completed input {
  accent-color: #4ade80;
}

.task-list li.completed span {
  text-decoration: line-through;
  color: var(--text-secondary);
}

.empty {
  text-align: center;
  padding: 1.5rem;
  color: var(--text-secondary);
}

.notification-item {
  display: flex;
  align-items: flex-start;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--border-color);
}

.notification-item:last-child {
  border-bottom: none;
}

.dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 12px;
  margin-top: 6px;
}

.dot.warning {
  background: var(--warning);
}

/* 响应式 */
@media (max-width: 768px) {
  .main-content {
    flex-direction: column;
  }
  
  .sidebar {
    width: 100%;
    border-right: none;
    border-bottom: 1px solid var(--border-color);
  }
  
  .cards-grid {
    grid-template-columns: 1fr;
  }
}
+ 在线测试

2.4 useReducer - 复杂状态管理

作用:复杂状态逻辑的替代方案(类似 Redux)

2.4.1 使用 useReducer 示例

  • useReducer 用于管理复杂的状态逻辑.它是 useState 的替代方案,特别适合处理包含多个子值的状态对象,或者下一个状态依赖于前一个状态的情况.
package.json
index.html
index.js
App.jsx
App.css
{
  "name": "react",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build --base ./"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.2.1",
    "vite": "^5.0.8"
  }
}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Hello World</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index.jsx"></script>
  </body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
import React, { useReducer, useEffect, createContext, useContext } from 'react';
import './App.css';

// 🔹 1. 定义 Action 类型(推荐用常量)
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const UPDATE_QUANTITY = 'UPDATE_QUANTITY';
const CLEAR_CART = 'CLEAR_CART';
const APPLY_COUPON = 'APPLY_COUPON';
const REMOVE_COUPON = 'REMOVE_COUPON';

// 🔹 2. 初始状态
const initialState = {
  items: [],
  coupon: null, // { code: 'SAVE10', discount: 10 }
  total: 0,
  subtotal: 0,
  discount: 0
};

// 🔹 3. Reducer 函数(核心逻辑)
const cartReducer = (state, action) => {
  switch (action.type) {
    case ADD_ITEM: {
      const { item } = action;
      const existingItem = state.items.find(i => i.id === item.id);
      
      let newItems;
      if (existingItem) {
        // 商品已存在:数量 +1
        newItems = state.items.map(i =>
          i.id === item.id 
            ? { ...i, quantity: i.quantity + 1 } 
            : i
        );
      } else {
        // 新商品:添加到列表
        newItems = [...state.items, { ...item, quantity: 1 }];
      }
      
      return calculateTotals({ ...state, items: newItems });
    }

    case REMOVE_ITEM: {
      const newItems = state.items.filter(i => i.id !== action.id);
      return calculateTotals({ ...state, items: newItems });
    }

    case UPDATE_QUANTITY: {
      const { id, quantity } = action;
      if (quantity < 1) return state; // 防止数量 ≤0
      
      const newItems = state.items.map(item =>
        item.id === id ? { ...item, quantity } : item
      );
      
      return calculateTotals({ ...state, items: newItems });
    }

    case CLEAR_CART:
      return { ...initialState };

    case APPLY_COUPON: {
      const { code, discount } = action;
      return {
        ...state,
        coupon: { code, discount },
        discount: discount
      };
    }

    case REMOVE_COUPON:
      return {
        ...state,
        coupon: null,
        discount: 0
      };

    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
};

// 🔹 4. 辅助函数:计算总价
const calculateTotals = (state) => {
  const subtotal = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  const discount = state.coupon ? state.coupon.discount : 0;
  const total = Math.max(0, subtotal - discount); // 防止负数

  return {
    ...state,
    subtotal: parseFloat(subtotal.toFixed(2)),
    discount: parseFloat(discount.toFixed(2)),
    total: parseFloat(total.toFixed(2))
  };
};

// 🔹 5. 创建 Context(可选:用于跨组件共享)
const CartContext = createContext();

// 🔹 6. 自定义 Hook(推荐用法)
const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within CartProvider');
  }
  return context;
};

// 🔹 7. Provider 组件
const CartProvider = ({ children }) => {
  const [state, dispatch] = useReducer(cartReducer, initialState, () => {
    // 初始化:从 localStorage 恢复(带验证)
    const saved = localStorage.getItem('shopping-cart');
    if (saved) {
      try {
        const parsed = JSON.parse(saved);
        // 验证结构
        if (Array.isArray(parsed.items)) {
          return calculateTotals(parsed);
        }
      } catch (e) {
        console.warn('Failed to parse cart from localStorage');
      }
    }
    return initialState;
  });

  // 🔸 useEffect:持久化状态到 localStorage
  useEffect(() => {
    localStorage.setItem('shopping-cart', JSON.stringify(state));
  }, [state]);

  // 操作方法(封装 dispatch)
  const addItem = (item) => dispatch({ type: ADD_ITEM, item });
  const removeItem = (id) => dispatch({ type: REMOVE_ITEM, id });
  const updateQuantity = (id, quantity) => 
    dispatch({ type: UPDATE_QUANTITY, id, quantity });
  const clearCart = () => dispatch({ type: CLEAR_CART });
  const applyCoupon = (code, discount) => 
    dispatch({ type: APPLY_COUPON, code, discount });
  const removeCoupon = () => dispatch({ type: REMOVE_COUPON });

  return (
    <CartContext.Provider value={{
      ...state,
      addItem,
      removeItem,
      updateQuantity,
      clearCart,
      applyCoupon,
      removeCoupon
    }}>
      {children}
    </CartContext.Provider>
  );
};

// 🔹 8. 商品列表组件
const ProductList = () => {
  const { addItem } = useCart();
  
  const products = [
    { id: 1, name: '无线蓝牙耳机', price: 299, image: '🎧' },
    { id: 2, name: '机械键盘', price: 599, image: '⌨️' },
    { id: 3, name: '27寸显示器', price: 1299, image: '🖥️' },
    { id: 4, name: 'Type-C 数据线', price: 39, image: '🔌' }
  ];

  return (
    <div className="products">
      <h2>热门商品</h2>
      <div className="product-grid">
        {products.map(product => (
          <div key={product.id} className="product-card">
            <div className="product-icon">{product.image}</div>
            <h3>{product.name}</h3>
            <p className="price">¥{product.price}</p>
            <button 
              onClick={() => addItem(product)}
              className="add-btn"
            >
              加入购物车
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

// 🔹 9. 购物车组件
const ShoppingCart = () => {
  const {
    items,
    subtotal,
    discount,
    total,
    coupon,
    updateQuantity,
    removeItem,
    applyCoupon,
    removeCoupon
  } = useCart();

  const handleApplyCoupon = () => {
    const code = prompt('输入优惠码(示例:SAVE10 → 减10元):');
    if (!code) return;
    
    // 简单验证:SAVE10=10元, SAVE20=20元
    const discountMap = { SAVE10: 10, SAVE20: 20 };
    const discount = discountMap[code.toUpperCase()];
    
    if (discount) {
      applyCoupon(code, discount);
      alert(`✅ 优惠码 ${code} 已生效!立减 ¥${discount}`);
    } else {
      alert('❌ 无效优惠码');
    }
  };

  return (
    <div className="cart">
      <h2>🛒 购物车 ({items.length} )</h2>
      
      {items.length === 0 ? (
        <div className="empty-cart">
          <p>购物车空空如也</p>
          <button onClick={() => document.getElementById('products').scrollIntoView()}>
            去逛逛
          </button>
        </div>
      ) : (
        <>
          <ul className="cart-items">
            {items.map(item => (
              <li key={item.id} className="cart-item">
                <span className="item-name">{item.name}</span>
                <div className="item-controls">
                  <button 
                    onClick={() => updateQuantity(item.id, item.quantity - 1)}
                    disabled={item.quantity <= 1}
                  >
                    -
                  </button>
                  <span className="quantity">{item.quantity}</span>
                  <button 
                    onClick={() => updateQuantity(item.id, item.quantity + 1)}
                  >
                    +
                  </button>
                  <span className="item-price">
                    ¥{(item.price * item.quantity).toFixed(2)}
                  </span>
                  <button 
                    className="remove-btn"
                    onClick={() => removeItem(item.id)}
                    title="删除"
                  >
                    ×
                  </button>
                </div>
              </li>
            ))}
          </ul>

          <div className="coupon-section">
            <input 
              type="text" 
              placeholder="输入优惠码"
              value={coupon?.code || ''}
              readOnly
              className="coupon-input"
            />
            {coupon ? (
              <button onClick={removeCoupon} className="coupon-btn remove">
                移除
              </button>
            ) : (
              <button onClick={handleApplyCoupon} className="coupon-btn apply">
                使用
              </button>
            )}
          </div>

          <div className="summary">
            <div className="summary-row">
              <span>商品总额</span>
              <span>¥{subtotal.toFixed(2)}</span>
            </div>
            {discount > 0 && (
              <div className="summary-row discount">
                <span>优惠 ({coupon?.code})</span>
                <span>-¥{discount.toFixed(2)}</span>
              </div>
            )}
            <div className="summary-row total">
              <strong>实付总额</strong>
              <strong>¥{total.toFixed(2)}</strong>
            </div>
          </div>

          <button className="checkout-btn" disabled={total === 0}>
            立即结算 (¥{total.toFixed(2)})
          </button>
        </>
      )}
    </div>
  );
};

// 🔹 10. 主应用
function App() {
  return (
    <CartProvider>
      <div className="app">
        <header className="header">
          <h1>🛒 React 购物车系统</h1>
          <p>useReducer 实战示例 | 数据自动保存至 localStorage</p>
        </header>
        
        <main className="main">
          <section id="products">
            <ProductList />
          </section>
          <aside>
            <ShoppingCart />
          </aside>
        </main>
        
        <footer className="footer">
          💡 尝试添加商品  修改数量  使用优惠码 SAVE10  SAVE20  刷新页面
        </footer>
      </div>
    </CartProvider>
  );
}

export default App;
/* App.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background-color: #f8f9fa;
  color: #333;
  line-height: 1.6;
}

.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  text-align: center;
  margin-bottom: 30px;
  padding: 20px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}

.header h1 {
  font-size: 2.2rem;
  color: #2c3e50;
  margin-bottom: 10px;
}

.header p {
  color: #7f8c8d;
  font-size: 1.1rem;
}

.main {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 24px;
}

/* 商品列表 */
.products h2 {
  font-size: 1.8rem;
  margin-bottom: 20px;
  color: #2c3e50;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.product-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  text-align: center;
  box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  transition: transform 0.2s, box-shadow 0.2s;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}

.product-icon {
  font-size: 3rem;
  margin-bottom: 12px;
}

.product-card h3 {
  font-size: 1.3rem;
  margin: 10px 0;
  color: #2c3e50;
}

.price {
  font-size: 1.4rem;
  color: #e74c3c;
  font-weight: bold;
  margin: 10px 0;
}

.add-btn {
  background: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
}

.add-btn:hover {
  background: #2980b9;
}

/* 购物车 */
.cart {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  position: sticky;
  top: 20px;
  height: fit-content;
}

.cart h2 {
  font-size: 1.6rem;
  margin-bottom: 20px;
  color: #2c3e50;
  display: flex;
  align-items: center;
  gap: 8px;
}

.empty-cart {
  text-align: center;
  padding: 40px 20px;
  color: #7f8c8d;
}

.empty-cart button {
  margin-top: 16px;
  background: #3498db;
  color: white;
  border: none;
  padding: 10px 24px;
  border-radius: 6px;
  cursor: pointer;
}

/* 购物车项 */
.cart-items {
  list-style: none;
  margin-bottom: 20px;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid #eee;
}

.item-name {
  flex: 1;
  font-weight: 500;
}

.item-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

.item-controls button {
  width: 32px;
  height: 32px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
}

.item-controls button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.quantity {
  width: 36px;
  text-align: center;
  font-weight: 500;
}

.item-price {
  min-width: 70px;
  text-align: right;
  font-weight: 500;
}

.remove-btn {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #ff6b6b;
  color: white;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

/* 优惠券 */
.coupon-section {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}

.coupon-input {
  flex: 1;
  padding: 10px 12px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 1rem;
}

.coupon-btn {
  padding: 10px 16px;
  border: none;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
}

.coupon-btn.apply {
  background: #2ecc71;
  color: white;
}

.coupon-btn.remove {
  background: #e74c3c;
  color: white;
}

/* 总计 */
.summary {
  margin-bottom: 20px;
}

.summary-row {
  display: flex;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 1.1rem;
}

.summary-row.discount {
  color: #27ae60;
}

.summary-row.total {
  font-size: 1.3rem;
  padding-top: 12px;
  border-top: 1px solid #eee;
}

.checkout-btn {
  width: 100%;
  padding: 14px;
  background: #e74c3c;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1.2rem;
  font-weight: bold;
  cursor: pointer;
  transition: background 0.2s;
}

.checkout-btn:disabled {
  background: #bdc3c7;
  cursor: not-allowed;
}

.checkout-btn:hover:not(:disabled) {
  background: #c0392b;
}

/* 页脚 */
.footer {
  text-align: center;
  margin-top: 30px;
  padding: 20px;
  color: #7f8c8d;
  font-size: 0.95rem;
}

/* 响应式 */
@media (max-width: 768px) {
  .main {
    grid-template-columns: 1fr;
  }
  
  .cart {
    position: static;
  }
}
+ 在线测试

2.5 useRef - 引用 DOM 或存储可变值

  • useRef 用于创建可变的引用对象,这些引用对象在组件的整个生命周期中保持不变。它的主要特点是:
  • 返回一个可变的对象:{ current: initialValue }
  • 不会触发组件重新渲染:修改 .current 不会导致组件重新渲染
  • 跨渲染周期保持稳定:每次渲染返回的是同一个对象
  • 与实例变量类似:类似于类组件中的实例属性
import { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  // 1. 创建 ref
  const inputRef = useRef(null);
  const countRef = useRef(0); // 存储可变值,不会触发重新渲染
  const prevValueRef = useRef('');

  // 2. 访问 DOM
  const focusInput = () => {
    inputRef.current.focus();
    inputRef.current.select();
  };

  useEffect(() => {
    // 记录渲染次数
    countRef.current += 1;
    console.log(`组件渲染了 ${countRef.current} 次`);
  });

  // 3. 存储上一次的值
  useEffect(() => {
    prevValueRef.current = inputRef.current?.value || '';
  });

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        placeholder="输入一些文字"
      />
      <button onClick={focusInput}>
        聚焦输入框
      </button>
      <p>渲染次数: {countRef.current}</p>
      <p>上一次的值: {prevValueRef.current}</p>
    </div>
  );
}
+ 在线测试

2.6 useMemouseCallback - 性能优化

作用:避免不必要的计算和重新渲染

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

function ExpensiveCalculation({ num }) {
  // useMemo: 缓存计算结果
  const expensiveResult = useMemo(() => {
    console.log('正在执行昂贵的计算...');
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += i * num;
    }
    return result;
  }, [num]); // 只有当 num 变化时才重新计算

  return <div>计算结果: {expensiveResult}</div>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(1);

  // useCallback: 缓存函数,避免子组件不必要的重新渲染
  const handleClick = useCallback(() => {
    console.log('点击处理');
    setCount(c => c + 1);
  }, []); // 依赖数组为空,函数不会重新创建

  // 不好的写法:每次渲染都会创建新函数
  // const badHandleClick = () => setCount(count + 1);

  return (
    <div>
      <ExpensiveCalculation num={num} />
      <p>计数: {count}</p>
      <button onClick={handleClick}>增加计数</button>
      <button onClick={() => setNum(num + 1)}>改变计算参数</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

// React.memo 配合 useCallback 使用
const ChildComponent = React.memo(({ onClick }) => {
  console.log('子组件渲染了');
  return <button onClick={onClick}>子组件按钮</button>;
});

+ 在线测试

三、自定义 Hooks - 逻辑复用

3.1 创建自定义 Hook

// useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 从 localStorage 读取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // 当值变化时保存到 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// 使用自定义 Hook
function App() {
  const [name, setName] = useLocalStorage('username', '张三');
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="输入用户名"
      />
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <p>保存的用户名: {name}</p>
      <p>当前主题: {theme}</p>
    </div>
  );
}

3.2 更多自定义 Hook 示例

// useFetch.js - 数据获取 Hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();

    async function fetchData() {
      try {
        const response = await fetch(url, {
          signal: abortController.signal
        });
        if (!response.ok) throw new Error('请求失败');
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// useWindowSize.js - 窗口尺寸 Hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

四、Hooks 使用规则

4.1 两个黄金规则

  1. 只在最顶层使用 Hooks
    • 不要在循环、条件或嵌套函数中调用 Hooks
    • 确保每次组件渲染时 Hooks 的调用顺序相同
// ✅ 正确:在顶层调用
function MyComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => { /* ... */ });
  return <div>{count}</div>;
}

// ❌ 错误:在条件中调用
function BadComponent({ shouldUse }) {
  if (shouldUse) {
    const [count, setCount] = useState(0); // 错误!
  }
  // ...
}

// ❌ 错误:在循环中调用
function AnotherBadComponent() {
  const items = [1, 2, 3];
  items.forEach(item => {
    const [value, setValue] = useState(item); // 错误!
  });
  // ...
}
  1. 只在 React 函数中调用 Hooks
    • 在 React 函数组件中调用
    • 在自定义 Hook 中调用

4.2 ESLint 插件

安装 ESLint 插件自动检查规则:

npm install eslint-plugin-react-hooks --save-dev

配置:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

五、Hooks 与 Class 组件对比

特性 Class 组件 函数组件 + Hooks
状态管理 this.state, this.setState() useState, useReducer
生命周期 componentDidMount, componentDidUpdate useEffect
Context static contextTypeContext.Consumer useContext
性能优化 shouldComponentUpdate, PureComponent useMemo, useCallback, React.memo
逻辑复用 高阶组件、Render Props 自定义 Hooks
代码量 较多 较少
this 绑定 需要处理 不需要
学习曲线 较陡峭 较平缓

六、实用技巧和最佳实践

6.1 处理依赖数组

function ProductList({ category, userId }) {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetchProducts(category, userId);
  }, [category, userId]); // ✅ 正确:包含所有依赖

  // ❌ 错误:缺少依赖
  // useEffect(() => {
  //   fetchProducts(category, userId);
  // }, [category]); // 缺少 userId

  // ✅ 如果函数定义在 useEffect 内部,不需要添加依赖
  useEffect(() => {
    async function fetchProducts() {
      const result = await fetch(`/api/products?category=${category}`);
      setProducts(await result.json());
    }
    fetchProducts();
  }, [category]);

  return <div>{/* ... */}</div>;
}

6.2 使用 useReducer 管理复杂状态

const initialState = {
  loading: false,
  data: null,
  error: null
};

function fetchReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { loading: false, data: action.payload, error: null };
    case 'FETCH_ERROR':
      return { loading: false, data: null, error: action.payload };
    default:
      return state;
  }
}

function useAsync(fetchFn) {
  const [state, dispatch] = useReducer(fetchReducer, initialState);

  const execute = useCallback(async (...args) => {
    dispatch({ type: 'FETCH_START' });
    try {
      const data = await fetchFn(...args);
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
      return data;
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
      throw error;
    }
  }, [fetchFn]);

  return { ...state, execute };
}

6.3 避免无限循环

// ❌ 错误:导致无限循环
function BadExample() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // 每次渲染都更新 state,导致无限循环
  }); // 缺少依赖数组
  
  return <div>{count}</div>;
}

// ✅ 正确:使用函数式更新
function GoodExample() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1); // 使用函数式更新
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // 空依赖数组,只运行一次
  
  return <div>{count}</div>;
}