# React Hooks

# Hooks 简介

Hooks 是 React 16.8 引入的特性,它允许在函数组件中使用状态和其他 React 特性,而无需编写类组件。Hooks 的出现解决了类组件存在的一些问题,并使函数组件的功能得到了极大的增强。

# 为什么需要 Hooks

  1. 类组件的问题

    • 组件逻辑难以复用(高阶组件和 render props 模式冗长复杂)
    • 复杂组件难以理解(生命周期方法混杂业务逻辑和副作用)
    • this 关键字增加理解负担(绑定事件处理器需要额外处理)
  2. Hooks 的优势

    • 无需学习类的复杂概念(如 this)
    • 更好的逻辑复用(通过自定义 Hooks)
    • 将相关逻辑组织在一起(按功能分组而非生命周期分散)
    • 函数式编程风格(更清晰、更容易测试)

# 基础 Hooks

# useState - 状态管理

import { useState } from 'react';

function Counter() {
  // 声明一个叫 count 的 state 变量,初始值为 0
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState 的特点

  • 返回一个状态值和一个更新函数
  • 更新函数可以接收新值或函数(函数接收先前的值并返回新值)
  • 初始状态可以是简单值或复杂对象
  • 多次调用 useState 可以使用多个状态变量

函数式更新

// 不依赖于之前的 state
setCount(count + 1);

// 依赖于之前的 state(推荐方式)
setCount(prevCount => prevCount + 1);

使用对象状态

const [user, setUser] = useState({ name: '', age: 0 });

// 更新部分状态(需要手动合并)
setUser(prevUser => ({ ...prevUser, name: 'John' }));

# useEffect - 副作用处理

import { useState, useEffect } from 'react';

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

  // 类似于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    document.title = `You clicked ${count} times`;
    
    // 返回清理函数(类似 componentWillUnmount)
    return () => {
      document.title = 'React App';
    };
  }, [count]); // 仅在 count 更改时执行

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

useEffect 的特点

  • 默认情况下,每次渲染后都会执行
  • 第二个参数是依赖数组,只有依赖变化时才会重新执行
  • 返回一个清理函数,在组件卸载或下一次 effect 执行前调用
  • 可以在同一组件中多次使用

常见用例

  • 数据获取
  • 事件监听
  • DOM 操作
  • 订阅设置

依赖数组说明

  • []:仅在挂载和卸载时执行(类似 componentDidMount/componentWillUnmount)
  • [dep1, dep2]:在 dep1 或 dep2 变化时执行
  • 不提供依赖数组:每次渲染后执行

# useContext - 上下文管理

import { createContext, useContext } from 'react';

// 创建上下文
const ThemeContext = createContext('light');

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

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

function ThemedButton() {
  // 使用上下文
  const theme = useContext(ThemeContext);
  return <button className={theme}>I am styled by theme context!</button>;
}

useContext 的特点

  • 接收一个 Context 对象作为参数
  • 返回该 Context 的当前值
  • 当 Provider 更新时,使用该 Context 的组件会重新渲染
  • 可以消费多个 Context

# 额外的 Hooks

# useReducer - 复杂状态逻辑

import { useReducer } from 'react';

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

function Counter() {
  // 使用 reducer
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

useReducer 的特点

  • 适合处理复杂的状态逻辑
  • 基于 action 的状态更新方式,类似 Redux
  • 可以将状态逻辑与 UI 分离
  • 当状态逻辑较复杂时,相比 useState 更适用

# useCallback - 记忆回调函数

import { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 只有当 count 变化时,handleClick 才会重新创建
  const handleClick = useCallback(() => {
    console.log(`Clicked, count is: ${count}`);
  }, [count]);
  
  return <ChildComponent onClick={handleClick} />;
}

// 使用 React.memo 避免不必要的重渲染
const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

useCallback 的特点

  • 返回一个记忆化的回调函数
  • 只有依赖项变化时才会重新创建函数
  • 用于优化传递给子组件的回调函数
  • 通常与 React.memo 一起使用效果更佳

# useMemo - 记忆计算结果

import { useState, useMemo } from 'react';

function ExpensiveCalculation({ a, b }) {
  // 只有当 a 或 b 变化时,result 才会重新计算
  const result = useMemo(() => {
    console.log('Computing result...');
    return a * b * 1000; // 假设这是一个复杂计算
  }, [a, b]);
  
  return <div>Result: {result}</div>;
}

useMemo 的特点

  • 记忆计算结果,避免每次渲染时重复计算
  • 只有依赖项变化时才会重新计算
  • 适用于计算量大的操作
  • 可以用来缓存对象,避免不必要的渲染

# useRef - 引用值/DOM

import { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  // 创建一个 ref
  const inputRef = useRef(null);
  
  // 点击按钮时聚焦输入框
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

useRef 的两个主要用途

  1. 引用 DOM 元素

    • 访问 DOM 节点进行操作(如聚焦、测量等)
    • 与第三方 DOM 库集成
  2. 保存可变值

    function Timer() {
      const intervalRef = useRef(null);
      
      useEffect(() => {
        intervalRef.current = setInterval(() => {
          // 执行某些操作
        }, 1000);
        
        return () => {
          clearInterval(intervalRef.current);
        };
      }, []);
      
      // ...
    }
    

# React 18 中的新 Hooks

# useTransition - 非阻塞更新

import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (e) => {
    // 立即更新输入框(高优先级)
    setQuery(e.target.value);
    
    // 将搜索结果更新标记为低优先级
    startTransition(() => {
      // 这里可能是一个耗时的操作
      const searchResults = performSearch(e.target.value);
      setResults(searchResults);
    });
  };
  
  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <div>Loading...</div> : (
        <ul>
          {results.map(result => (
            <li key={result.id}>{result.name}</li>
          ))}
        </ul>
      )}
    </>
  );
}

useTransition 的特点

  • 将状态更新标记为非紧急
  • 允许其他更新打断当前更新
  • 提供 isPending 标志指示过渡状态
  • 用于改善大型状态更新的用户体验

# useDeferredValue - 延迟值

import { useState, useDeferredValue } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  // 创建查询的延迟版本
  const deferredQuery = useDeferredValue(query);
  
  // 使用延迟值渲染结果
  // 只有当 deferredQuery 变化时才会重新计算
  const results = useMemo(
    () => performSearch(deferredQuery),
    [deferredQuery]
  );
  
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <Results results={results} />
    </>
  );
}

useDeferredValue 的特点

  • 创建一个延迟版本的值
  • 当原始值变化时,会先保持旧值,然后在后台更新
  • 适用于防止大型列表或复杂组件的渲染阻塞用户输入
  • 与 useTransition 类似,但更适合处理接收到的 props

# 自定义 Hooks

自定义 Hooks 是 React 中重用状态逻辑的主要方式。它们是普通的 JavaScript 函数,名称以 "use" 开头,可以调用其他 Hooks。

# 例子:useLocalStorage

import { useState, useEffect } from 'react';

// 自定义 Hook 用于本地存储
function useLocalStorage(key, initialValue) {
  // 初始化状态
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // 尝试从本地存储获取值
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(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];
}

// 使用自定义 Hook
function App() {
  const [name, setName] = useLocalStorage('name', 'Bob');
  
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
      />
    </div>
  );
}

# 例子:useFetch

import { useState, useEffect } from 'react';

// 自定义 Hook 用于数据获取
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;
    
    const fetchData = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(url, { signal });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        if (!signal.aborted) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (!signal.aborted) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (!signal.aborted) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      abortController.abort();
    };
  }, [url]);
  
  return { data, loading, error };
}

// 使用自定义 Hook
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <h1>{data.name}</h1>
      <p>Email: {data.email}</p>
    </div>
  );
}

# Hooks 最佳实践

  1. 只在顶层调用 Hooks

    • 不要在循环、条件或嵌套函数中调用 Hooks
    • 确保 Hooks 的调用顺序保持一致
  2. 只在 React 函数中调用 Hooks

    • 在 React 函数组件中调用 Hooks
    • 在自定义 Hooks 中调用 Hooks
    • 不要在普通 JavaScript 函数中调用 Hooks
  3. 依赖数组的处理

    • 包含所有 effect 中使用的外部变量
    • 使用 ESLint 插件 eslint-plugin-react-hooks 检查依赖
    • 考虑使用函数式更新或 useReducer 减少依赖
  4. 自定义 Hooks 设计

    • 以 "use" 开头命名
    • 函数签名设计要清晰直观
    • 返回值设计简洁(数组或对象)
    • 处理好内部的错误和加载状态
  5. 性能优化技巧

    • 使用 useMemo 缓存复杂计算
    • 使用 useCallback 记忆事件处理器
    • 拆分状态,避免不必要的渲染
    • 考虑使用 React.memo 包装函数组件

# 从类组件迁移到 Hooks

类组件 Hooks
constructor useState
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {})
componentWillUnmount useEffect(() => { return () => {} }, [])
shouldComponentUpdate React.memo
静态方法和属性 常规函数和变量

# 结论

Hooks 彻底改变了 React 的组件编写方式,使函数组件成为了首选方案。通过 Hooks,开发者可以将相关逻辑组织在一起,而不是分散在各个生命周期方法中。自定义 Hooks 还提供了一种干净、强大的方式来重用状态逻辑,而不需要依赖复杂的高阶组件或 render props 模式。

随着 React 的发展,Hooks 已经成为 React 生态系统中不可或缺的部分,掌握它们对于任何 React 开发者来说都至关重要。