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 useMemo 和 useCallback - 性能优化

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

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 contextType 或 Context.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>;
}