React Hooks 使用详解

引言

React Hooks 是 React 16.8 版本引入的重要特性,它彻底改变了函数组件的开发方式,使得函数组件可以拥有状态(state)和生命周期等原本只有类组件才具备的能力。本文将深入探讨 React Hooks 的各个方面,从基础概念到高级用法,帮助你全面掌握这一现代 React 开发的核心技术。

1. React Hooks 基础概念

1.1 什么是 Hooks

Hooks 是 React 提供的一组函数,让你可以在函数组件中"钩入"React 的状态和生命周期等特性。它们以 use 开头命名,例如 useState、useEffect 等。

1.2 为什么需要 Hooks

在 Hooks 出现之前,React 有两种创建组件的方式:

  • 函数组件:简单但功能有限,无法使用状态和生命周期
  • 类组件:功能完整但代码冗长,学习成本高,难以复用逻辑

Hooks 的出现解决了以下问题:

  • 在不编写 class 的情况下使用 state 和其他 React 特性
  • 更容易复用组件状态逻辑
  • 简化复杂组件的理解和测试
  • 避免类组件中 this 指向的问题

2. 基础 Hooks

2.1 useState

useState 是最基础的 Hook,用于在函数组件中添加状态。

jsx 复制代码
import React, { useState } from 'react';

function Counter() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState 的使用要点:

  1. 数组解构 :useState 返回一个数组,第一个元素是当前状态值,第二个元素是更新状态的函数
  2. 初始值 :useState 的参数是状态的初始值
  3. 状态更新:调用更新函数会触发组件重新渲染
  4. 函数式更新:当新状态依赖于前一个状态时,可以传入函数:
jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    // 函数式更新,确保基于最新状态计算
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

2.2 useEffect

useEffect 用于处理副作用,如数据获取、订阅或手动修改 DOM。

jsx 复制代码
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 的清理机制:

jsx 复制代码
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // 订阅好友状态
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    // 清理函数,在组件卸载时执行
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect 的依赖数组:

jsx 复制代码
// 1. 没有依赖数组 - 每次渲染后都执行
useEffect(() => {
  console.log('每次渲染后都执行');
});

// 2. 空依赖数组 - 只在挂载时执行一次
useEffect(() => {
  console.log('只在挂载时执行');
}, []);

// 3. 有依赖项 - 依赖项变化时执行
useEffect(() => {
  console.log('count 变化时执行');
}, [count]);

2.3 useContext

useContext 用于订阅 React context 的变更。

jsx 复制代码
import React, { useContext } from 'react';

// 创建 Context
const ThemeContext = React.createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  // 使用 useContext 获取 context 值
  const theme = useContext(ThemeContext);
  
  return (
    <button style={{ background: theme === 'dark' ? 'black' : 'white' }}>
      I am styled by theme context!
    </button>
  );
}

3. 高级 Hooks

3.1 useReducer

useReducer 是 useState 的替代方案,适用于复杂的状态逻辑。

jsx 复制代码
import React, { useReducer } from 'react';

// 定义 reducer 函数
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
        Reset
      </button>
    </>
  );
}

3.2 useCallback

useCallback 用于优化性能,返回一个 memoized 回调函数。

jsx 复制代码
import React, { useState, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 使用 useCallback 缓存函数,避免子组件不必要的重新渲染
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const handleNameChange = useCallback((e) => {
    setName(e.target.value);
  }, []);

  return (
    <div>
      <Child onIncrement={handleIncrement} />
      <input value={name} onChange={handleNameChange} />
    </div>
  );
}

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

3.3 useMemo

useMemo 用于优化性能,返回一个 memoized 值。

jsx 复制代码
import React, { useState, useMemo } from 'react';

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // 使用 useMemo 缓存昂贵的计算结果
  const expensiveValue = useMemo(() => {
    console.log('执行昂贵的计算');
    return items.reduce((acc, item) => acc + item.value, 0);
  }, [items]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Expensive Value: {expensiveValue}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setItems([...items, { value: Math.random() }])}>
        Add Item
      </button>
    </div>
  );
}

3.4 useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。

jsx 复制代码
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 直接访问 DOM 节点
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

// 保存可变值的示例
function Timer() {
  const intervalRef = useRef(null);
  const [count, setCount] = useState(0);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => {
      clearInterval(intervalRef.current);
    };
  }, []);

  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

4. 自定义 Hooks

自定义 Hooks 是 React 中复用组件逻辑的常用方式。

4.1 创建自定义 Hook

jsx 复制代码
import { useState, useEffect } from 'react';

// 自定义 Hook:获取当前窗口尺寸
function useWindowDimensions() {
  const [windowDimensions, setWindowDimensions] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

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

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

  return windowDimensions;
}

// 使用自定义 Hook
function ShowWindowDimensions() {
  const { width, height } = useWindowDimensions();

  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

4.2 更复杂的自定义 Hook

jsx 复制代码
import { useState, useEffect } from 'react';

// 自定义 Hook:数据获取
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// 使用自定义 Hook
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

5. Hooks 最佳实践

5.1 Hook 规则

  1. 只在最顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks
  2. 只在 React 函数组件中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hooks
jsx 复制代码
// ✅ 正确:在顶层调用
function MyComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // ...
  }, []);

  // ...
}

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

5.2 优化性能

jsx 复制代码
import React, { useState, useCallback, useMemo } from 'react';

function OptimizedComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 使用 useCallback 缓存函数
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  // 使用 useMemo 缓存计算结果
  const expensiveCalculation = useMemo(() => {
    // 模拟昂贵的计算
    let result = 0;
    for (let i = 0; i < count * 1000000; i++) {
      result += i;
    }
    return result;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Calculation: {expensiveCalculation}</p>
      <button onClick={handleIncrement}>Increment</button>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Enter name"
      />
    </div>
  );
}

5.3 错误处理

jsx 复制代码
import { useState, useEffect } from 'react';

function useAsyncData(fetchFunction) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const result = await fetchFunction();
        
        // 确保组件仍然挂载
        if (isMounted) {
          setData(result);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // 清理函数
    return () => {
      isMounted = false;
    };
  }, [fetchFunction]);

  return { data, loading, error };
}

6. 实际应用场景

6.1 表单处理

jsx 复制代码
import React, { useState } from 'react';

function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
    
    // 清除对应字段的错误
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: null
      }));
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // 这里可以添加表单验证逻辑
    onSubmit(values);
  };

  return {
    values,
    errors,
    handleChange,
    handleSubmit
  };
}

function ContactForm() {
  const { values, handleChange, handleSubmit } = useForm({
    name: '',
    email: '',
    message: ''
  }, (formData) => {
    console.log('提交表单:', formData);
    // 处理表单提交逻辑
  });

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Name:</label>
        <input
          type="text"
          name="name"
          value={values.name}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>Message:</label>
        <textarea
          name="message"
          value={values.message}
          onChange={handleChange}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

6.2 数据获取和缓存

jsx 复制代码
import { useState, useEffect } from 'react';

// 简单的数据缓存实现
const cache = new Map();

function useCachedData(key, fetcher) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 检查缓存
    if (cache.has(key)) {
      setData(cache.get(key));
      setLoading(false);
      return;
    }

    const fetchData = async () => {
      try {
        setLoading(true);
        const result = await fetcher();
        cache.set(key, result);
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [key, fetcher]);

  return { data, loading, error };
}

// 使用示例
function UserList() {
  const { data: users, loading, error } = useCachedData(
    'users',
    () => fetch('/api/users').then(res => res.json())
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

6.3 动画和过渡效果

jsx 复制代码
import { useState, useEffect, useRef } from 'react';

function useAnimation(initialValue, targetValue, duration = 300) {
  const [currentValue, setCurrentValue] = useState(initialValue);
  const startTimeRef = useRef(null);
  const animationRef = useRef(null);

  useEffect(() => {
    if (startTimeRef.current === null) {
      startTimeRef.current = performance.now();
    }

    const animate = (timestamp) => {
      if (!startTimeRef.current) startTimeRef.current = timestamp;
      
      const elapsed = timestamp - startTimeRef.current;
      const progress = Math.min(elapsed / duration, 1);
      
      // 简单的线性插值
      const newValue = initialValue + (targetValue - initialValue) * progress;
      setCurrentValue(newValue);

      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate);
      } else {
        startTimeRef.current = null;
      }
    };

    animationRef.current = requestAnimationFrame(animate);

    return () => {
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current);
      }
    };
  }, [initialValue, targetValue, duration]);

  return currentValue;
}

function AnimatedCounter() {
  const [target, setTarget] = useState(0);
  const animatedValue = useAnimation(0, target, 1000);

  return (
    <div>
      <p>Animated Value: {Math.round(animatedValue)}</p>
      <button onClick={() => setTarget(target + 100)}>
        Increase Target
      </button>
    </div>
  );
}

7. 常见陷阱和解决方案

7.1 无限循环问题

jsx 复制代码
// ❌ 错误:会导致无限循环
function BadComponent() {
  const [count, setCount] = useState(0);
  
  // 每次渲染都会创建新对象,导致 useEffect 无限执行
  const config = { theme: 'dark' };
  
  useEffect(() => {
    console.log('Effect executed');
    // 一些副作用操作
  }, [config]); // config 每次都是新对象
  
  return <div>{count}</div>;
}

// ✅ 正确:使用 useMemo 或提取到组件外
function GoodComponent() {
  const [count, setCount] = useState(0);
  
  // 使用 useMemo 缓存对象
  const config = useMemo(() => ({ theme: 'dark' }), []);
  
  useEffect(() => {
    console.log('Effect executed');
  }, [config]);
  
  return <div>{count}</div>;
}

7.2 闭包陷阱

jsx 复制代码
// ❌ 错误:可能获取到过期的 state 值
function BadTimer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // 这里的 count 可能是过期的值
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []); // 依赖数组为空,count 不会更新

  return <div>Count: {count}</div>;
}

// ✅ 正确:使用函数式更新
function GoodTimer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // 使用函数式更新确保获取最新值
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>Count: {count}</div>;
}

7.3 条件渲染中的 Hook

jsx 复制代码
// ❌ 错误:Hook 的调用顺序不一致
function BadComponent({ showDetails }) {
  const [count, setCount] = useState(0);
  
  if (showDetails) {
    // 条件下调用 Hook,可能导致顺序不一致
    const [details, setDetails] = useState(null);
    // ...
  }
  
  return <div>{count}</div>;
}

// ✅ 正确:始终调用所有 Hook
function GoodComponent({ showDetails }) {
  const [count, setCount] = useState(0);
  const [details, setDetails] = useState(null);
  
  useEffect(() => {
    if (showDetails) {
      // 在 effect 中处理条件逻辑
      fetchDetails().then(setDetails);
    }
  }, [showDetails]);
  
  return <div>{count}</div>;
}

8. 测试 Hooks

8.1 使用 React Testing Library

jsx 复制代码
import { render, screen, fireEvent } from '@testing-library/react';
import { useState, useEffect } from 'react';

// 测试组件
function TestComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);
  
  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

// 测试用例
test('increments count when button is clicked', () => {
  render(<TestComponent />);
  
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  
  fireEvent.click(screen.getByText('Increment'));
  
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

8.2 测试自定义 Hook

jsx 复制代码
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

// 自定义 Hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}

// 测试自定义 Hook
test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());
  
  expect(result.current.count).toBe(0);
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

test('should decrement counter', () => {
  const { result } = renderHook(() => useCounter(5));
  
  act(() => {
    result.current.decrement();
  });
  
  expect(result.current.count).toBe(4);
});

test('should reset counter', () => {
  const { result } = renderHook(() => useCounter(5));
  
  act(() => {
    result.current.increment();
    result.current.increment();
    result.current.reset();
  });
  
  expect(result.current.count).toBe(5);
});

9. 与其他状态管理方案的集成

9.1 与 Redux 集成

jsx 复制代码
import { useSelector, useDispatch } from 'react-redux';

function TodoList() {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

9.2 与 Context 集成

jsx 复制代码
import { createContext, useContext, useReducer } from 'react';

const AppContext = createContext();

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
}

function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
}

function Component() {
  const { state, dispatch } = useApp();
  // 使用 state 和 dispatch
}

10. 总结

React Hooks 彻底改变了 React 的开发方式,提供了更简洁、更灵活的状态和副作用管理方案。通过掌握基础 Hooks 和自定义 Hooks 的创建,我们可以构建更加可维护和可复用的组件。

关键要点回顾:

  1. 基础 Hooks :useState、useEffect、useContext 是最常用的 Hooks
  2. 高级 Hooks :useReducer、useCallback、useMemo、useRef 提供更多功能
  3. 自定义 Hooks:是复用逻辑的最佳方式
  4. 遵循规则:只在顶层调用 Hooks,只在 React 函数中调用 Hooks
  5. 性能优化 :合理使用 useCallback 和 useMemo
  6. 避免陷阱:注意闭包、无限循环等问题

随着 React 生态的不断发展,Hooks 已经成为现代 React 开发的标准,掌握它们对于任何 React 开发者来说都是至关重要的技能。

相关推荐
wangruofeng9 分钟前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng13 分钟前
给前端开发者的 100 个设计参考网站
前端·设计
星栈3 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技3 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569154 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马6 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng7 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding7 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天8 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo10 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构