自定义 Hook 开发

React 自定义 Hook 的概念、规则和常见实现

问题

什么是自定义 Hook?如何开发自定义 Hook?

解答

自定义 Hook 是以 use 开头的函数,内部可以调用其他 Hook,用于复用组件间的状态逻辑。

基本结构

// 自定义 Hook 必须以 use 开头
function useCustomHook(initialValue) {
  const [state, setState] = useState(initialValue);
  
  // 封装逻辑
  const doSomething = useCallback(() => {
    setState(/* ... */);
  }, []);
  
  // 返回状态和方法
  return [state, doSomething];
}

示例 1:useToggle

import { useState, useCallback } from 'react';

// 布尔值切换 Hook
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);
  
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return [value, toggle, setTrue, setFalse];
}

// 使用
function Modal() {
  const [isOpen, toggle, open, close] = useToggle(false);
  
  return (
    <div>
      <button onClick={open}>打开</button>
      {isOpen && (
        <div className="modal">
          <button onClick={close}>关闭</button>
        </div>
      )}
    </div>
  );
}

示例 2:useLocalStorage

import { useState, useEffect } from 'react';

// 持久化状态到 localStorage
function useLocalStorage(key, initialValue) {
  // 初始化时从 localStorage 读取
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  // 值变化时同步到 localStorage
  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('localStorage 写入失败:', error);
    }
  }, [key, value]);
  
  return [value, setValue];
}

// 使用
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="light">浅色</option>
      <option value="dark">深色</option>
    </select>
  );
}

示例 3: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 controller = new AbortController();
    
    async function fetchData() {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error('请求失败');
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }
    
    fetchData();
    
    // 清理:取消未完成的请求
    return () => controller.abort();
  }, [url]);
  
  return { data, loading, error };
}

// 使用
function UserList() {
  const { data, loading, error } = useFetch('/api/users');
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

示例 4:useDebounce

import { useState, useEffect } from 'react';

// 防抖 Hook
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    // 清理上一次的定时器
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用:搜索输入
function Search() {
  const [keyword, setKeyword] = useState('');
  const debouncedKeyword = useDebounce(keyword, 500);
  
  // 只在防抖后的值变化时请求
  const { data } = useFetch(
    debouncedKeyword ? `/api/search?q=${debouncedKeyword}` : null
  );
  
  return (
    <div>
      <input 
        value={keyword}
        onChange={e => setKeyword(e.target.value)}
        placeholder="搜索..."
      />
      {/* 渲染搜索结果 */}
    </div>
  );
}

关键点

  • 命名规则:必须以 use 开头,React 依此识别 Hook
  • 调用规则:只能在函数组件或其他 Hook 中调用,不能在条件语句中调用
  • 状态隔离:每次调用自定义 Hook 都会获得独立的状态
  • 返回值灵活:可返回数组、对象或单个值,根据使用场景决定
  • 组合使用:自定义 Hook 内部可以调用其他 Hook,实现逻辑组合