Chenyj Space
返回博客
·47 分钟

React Hooks 深入解析:从原理到实战

ReactHooks前端JavaScript前端

Hooks 基础概念

为什么需要 Hooks

类组件的问题

  1. 逻辑复用困难(HOC、Render Props 嵌套)
  2. 复杂组件难以理解(生命周期分散)
  3. this 指向问题

Hooks 的优势

  1. 逻辑复用更简单(自定义 Hooks)
  2. 代码更清晰(按功能组织)
  3. 无需类(函数组件更简洁)

Hooks 规则

jsx
// ✅ 规则1:只在顶层调用 Hooks
function MyComponent() {
  const [count, setCount] = useState(0);  // ✅ 顶层
  
  if (count > 0) {
    // ❌ 错误:不能在条件语句中调用
    // const [name, setName] = useState('');
  }
}

// ✅ 规则2:只在 React 函数组件或自定义 Hooks 中调用
function myHook() {
  const [value, setValue] = useState(0);  // ✅ 自定义 Hook
}

function regularFunction() {
  // ❌ 错误:普通函数中不能调用 Hooks
  // const [value, setValue] = useState(0);
}

useState 详解

基本用法

jsx
import { useState } from 'react';

function Counter() {
  // 声明状态
  const [count, setCount] = useState(0);
  
  // 更新状态
  const increment = () => {
    setCount(count + 1);
  };
  
  // 函数式更新(推荐)
  const incrementFunctional = () => {
    setCount(prev => prev + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={incrementFunctional}>+1 (Functional)</button>
    </div>
  );
}

惰性初始化

jsx
// ❌ 不推荐:每次渲染都会调用
const [state, setState] = useState(expensiveComputation());

// ✅ 推荐:只在首次渲染时调用
const [state, setState] = useState(() => {
  return expensiveComputation();
});

// 实际应用
function TodoList() {
  const [todos, setTodos] = useState(() => {
    // 从 localStorage 读取
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });
}

对象和数组状态

jsx
function Form() {
  // 对象状态
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  // ❌ 错误:会覆盖整个对象
  const updateName = (name) => {
    setUser({ name });  // email 和 age 丢失!
  };
  
  // ✅ 正确:展开运算符
  const updateName = (name) => {
    setUser(prev => ({ ...prev, name }));
  };
  
  // 数组状态
  const [items, setItems] = useState([]);
  
  // 添加元素
  const addItem = (item) => {
    setItems(prev => [...prev, item]);
  };
  
  // 删除元素
  const removeItem = (index) => {
    setItems(prev => prev.filter((_, i) => i !== index));
  };
  
  // 更新元素
  const updateItem = (index, newItem) => {
    setItems(prev => prev.map((item, i) => 
      i === index ? newItem : item
    ));
  };
}

useEffect 详解

基本用法

jsx
import { useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  // 每次渲染后执行
  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // 清理函数
    return () => {
      clearInterval(timer);
    };
  }, []);  // 空依赖数组:只在挂载和卸载时执行
  
  return <p>Seconds: {seconds}</p>;
}

依赖数组

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  // ❌ 每次渲染都执行(无限循环)
  useEffect(() => {
    fetchUser(userId).then(setUser);
  });
  
  // ✅ 只在 userId 变化时执行
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  // ✅ 只在挂载时执行
  useEffect(() => {
    console.log('Component mounted');
  }, []);
}

// 多个 useEffect
function Dashboard() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  // 用户数据
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);
  
  // 帖子数据
  useEffect(() => {
    fetchPosts().then(setPosts);
  }, []);
  
  // 用户和帖子都加载后的操作
  useEffect(() => {
    if (user && posts.length > 0) {
      console.log('Both loaded');
    }
  }, [user, posts]);
}

清理函数

jsx
function WindowSize() {
  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 (
    <p>Window size: {size.width} x {size.height}</p>
  );
}

// 实际应用:订阅和取消订阅
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const unsubscribe = subscribeToRoom(roomId, (message) => {
      setMessages(prev => [...prev, message]);
    });
    
    return () => {
      unsubscribe();
    };
  }, [roomId]);
}

useCallback 详解

问题场景

jsx
function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ 每次渲染都创建新函数
  const handleClick = () => {
    console.log('Clicked');
  };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      {/* Child 每次都会重新渲染 */}
      <Child onClick={handleClick} />
    </div>
  );
}

const Child = React.memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

使用 useCallback

jsx
function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);  // 空依赖:函数永远不变
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      {/* Child 只在 handleClick 变化时重新渲染 */}
      <Child onClick={handleClick} />
    </div>
  );
}

// 依赖项
function SearchComponent() {
  const [query, setQuery] = useState('');
  
  // 依赖 query 的函数
  const handleSearch = useCallback(() => {
    search(query);
  }, [query]);  // query 变化时重新创建函数
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

何时使用 useCallback

jsx
// ✅ 场景1:传递给 memo 组件
const MemoizedChild = React.memo(Child);
<MemoizedChild onClick={useCallback(() => {}, [])} />

// ✅ 场景2:作为其他 Hook 的依赖
useEffect(() => {
  fetchData();
}, [useCallback(() => fetchData(), [])]);

// ❌ 不需要:普通事件处理
function Button() {
  // 不需要 useCallback,因为没有性能问题
  const handleClick = () => {
    console.log('clicked');
  };
  
  return <button onClick={handleClick}>Click</button>;
}

useMemo 详解

问题场景

jsx
function ProductList({ products, filter }) {
  // ❌ 每次渲染都重新计算
  const filteredProducts = products.filter(p => 
    p.category === filter
  );
  
  // ❌ 每次渲染都重新排序
  const sortedProducts = filteredProducts.sort((a, b) => 
    a.price - b.price
  );
  
  return (
    <ul>
      {sortedProducts.map(p => (
        <li key={p.id}>{p.name}: ${p.price}</li>
      ))}
    </ul>
  );
}

使用 useMemo

jsx
function ProductList({ products, filter }) {
  // ✅ 只在 products 或 filter 变化时重新计算
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === filter);
  }, [products, filter]);
  
  // ✅ 只在 filteredProducts 变化时重新排序
  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort((a, b) => a.price - b.price);
  }, [filteredProducts]);
  
  return (
    <ul>
      {sortedProducts.map(p => (
        <li key={p.id}>{p.name}: ${p.price}</li>
      ))}
    </ul>
  );
}

// 实际应用:复杂计算
function Analytics({ data }) {
  // ✅ 缓存复杂计算结果
  const statistics = useMemo(() => {
    const sum = data.reduce((acc, val) => acc + val, 0);
    const avg = sum / data.length;
    const max = Math.max(...data);
    const min = Math.min(...data);
    
    return { sum, avg, max, min };
  }, [data]);
  
  return (
    <div>
      <p>Sum: {statistics.sum}</p>
      <p>Average: {statistics.avg}</p>
      <p>Max: {statistics.max}</p>
      <p>Min: {statistics.min}</p>
    </div>
  );
}

何时使用 useMemo

jsx
// ✅ 场景1:复杂计算
const result = useMemo(() => heavyComputation(data), [data]);

// ✅ 场景2:创建对象/数组(避免子组件重新渲染)
const options = useMemo(() => ({ key: value }), [value]);

// ✅ 场景3:作为其他 Hook 的依赖
const memoizedValue = useMemo(() => compute(a, b), [a, b]);
useEffect(() => {
  doSomething(memoizedValue);
}, [memoizedValue]);

// ❌ 不需要:简单计算
function Double({ number }) {
  // 不需要 useMemo,乘法很快
  const doubled = number * 2;
  return <p>{doubled}</p>;
}

useRef 详解

访问 DOM

jsx
function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

// 实际应用:表单验证
function Form() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (!nameRef.current.value) {
      nameRef.current.focus();
      return;
    }
    
    if (!emailRef.current.value) {
      emailRef.current.focus();
      return;
    }
    
    // 提交表单
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} placeholder="Name" />
      <input ref={emailRef} placeholder="Email" />
      <button type="submit">Submit</button>
    </form>
  );
}

保存可变值

jsx
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  
  const start = () => {
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(intervalRef.current);
  };
  
  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);
  
  return (
    <div>
      <p>Seconds: {seconds}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

// 保存前一个值
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <p>
      Current: {count}, Previous: {prevCount}
    </p>
  );
}

自定义 Hooks

基本结构

jsx
// 自定义 Hook:以 "use" 开头
function useLocalStorage(key, initialValue) {
  // 使用 useState
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });
  
  // 封装更新函数
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function 
        ? value(storedValue) 
        : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}

// 使用
function App() {
  const [name, setName] = useLocalStorage('name', '');
  
  return (
    <input 
      value={name} 
      onChange={e => setName(e.target.value)} 
    />
  );
}

常用自定义 Hooks

jsx
// 1. useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: abortController.signal
        });
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    return () => {
      abortController.abort();
    };
  }, [url]);
  
  return { data, loading, error };
}

// 2. useDebounce
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

// 3. useMediaQuery
function useMediaQuery(query) {
  const [matches, setMatches] = useState(
    window.matchMedia(query).matches
  );
  
  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    
    const handler = (event) => {
      setMatches(event.matches);
    };
    
    mediaQuery.addEventListener('change', handler);
    
    return () => {
      mediaQuery.removeEventListener('change', handler);
    };
  }, [query]);
  
  return matches;
}

// 4. useOnlineStatus
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
}

性能优化

React.memo

jsx
// 基本用法
const MemoizedComponent = React.memo(function MyComponent({ name }) {
  console.log('Rendered');
  return <p>Hello, {name}</p>;
});

// 自定义比较
const MemoizedComponent = React.memo(
  function MyComponent({ items }) {
    return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
  },
  (prevProps, nextProps) => {
    // 只在 items 长度变化时重新渲染
    return prevProps.items.length === nextProps.items.length;
  }
);

优化实践

jsx
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  
  // ✅ 缓存过滤后的列表
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => {
      if (filter === 'active') return !todo.completed;
      if (filter === 'completed') return todo.completed;
      return true;
    });
  }, [todos, filter]);
  
  // ✅ 缓存回调函数
  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);
  
  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);
  
  return (
    <div>
      <FilterButtons filter={filter} onFilterChange={setFilter} />
      <TodoItems 
        todos={filteredTodos} 
        onToggle={handleToggle}
        onDelete={handleDelete}
      />
    </div>
  );
}

const TodoItems = React.memo(({ todos, onToggle, onDelete }) => {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
});

const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{
        textDecoration: todo.completed ? 'line-through' : 'none'
      }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

常见陷阱

1. 闭包陷阱

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  // ❌ 问题:闭包捕获了旧的 count
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);  // 永远是 0 + 1
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // 空依赖
  
  // ✅ 解决:使用函数式更新
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1);  // 使用最新的值
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
}

2. 依赖项遗漏

jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  // ❌ 问题:query 变化时不会重新搜索
  useEffect(() => {
    fetchResults(query).then(setResults);
  }, []);  // 遗漏了 query
  
  // ✅ 正确:添加 query 到依赖数组
  useEffect(() => {
    fetchResults(query).then(setResults);
  }, [query]);
}

3. 对象依赖

jsx
function Component({ config }) {
  // ❌ 问题:config 对象每次都是新的
  useEffect(() => {
    doSomething(config);
  }, [config]);  // 每次渲染都执行
  
  // ✅ 解决:使用 useMemo 缓存对象
  const memoizedConfig = useMemo(() => config, [
    config.url,
    config.method
  ]);
  
  useEffect(() => {
    doSomething(memoizedConfig);
  }, [memoizedConfig]);
}

总结

核心 Hooks

Hook用途依赖项
useState状态管理
useEffect副作用依赖数组
useCallback缓存函数依赖数组
useMemo缓存值依赖数组
useRef引用/可变值

使用原则

  1. 按功能组织:将相关的 Hooks 放在一起
  2. 提取自定义 Hooks:复用状态逻辑
  3. 优化性能:合理使用 memo、useCallback、useMemo
  4. 避免过度优化:只在真正需要时优化

学习资源


延伸阅读