4、自定义 Hook

2 阅读4分钟

太棒了!👏 你已经掌握了 React 的核心机制,现在是时候“造轮子”了!


—— 封装可复用逻辑的艺术

💡 自定义 Hook 是 React 中最强大的抽象机制之一。
它让你把组件逻辑提取成可复用的函数,告别重复代码。


一、什么是自定义 Hook?🧩

✅ 定义:

一个以 use 开头的 JavaScript 函数,内部可以调用其他 Hook(如 useState, useEffect)。

🎯 目标:封装“状态逻辑”而非 UI

对比普通函数自定义 Hook
能否使用 Hook?❌ 不能✅ 可以
是否有状态?❌ 无✅ 有
用途工具函数(计算、格式化)状态 + 副作用逻辑封装

二、为什么需要自定义 Hook?🎯

想象你写了 5 个组件,每个都要:

  • 从 localStorage 读数据
  • 监听输入变化
  • 写回 localStorage

👉 你会复制粘贴 5 次吗?❌
✅ 应该封装成 useLocalStorage

✨ 自定义 Hook 解决三大问题:

  1. 逻辑复用(不是 UI 复用)
  2. 避免重复代码
  3. 提升组件可读性

三、实战案例:从零实现几个经典自定义 Hook 💡

🪝 示例 1:useLocalStorage —— 带持久化的 state

// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 从 localStorage 读取初始值
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`读取 ${key} 失败`, error);
      return initialValue;
    }
  });

  // 当 value 变化时,同步到 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`保存 ${key} 失败`, error);
    }
  }, [key, value]);

  return [value, setValue];
}

✅ 使用方式:

function DarkModeToggle() {
  const [isDark, setIsDark] = useLocalStorage('theme', false);

  return (
    <button onClick={() => setIsDark(!isDark)}>
      当前主题:{isDark ? '暗黑' : '明亮'}
    </button>
  );
}

✅ 刷新页面后状态依然保留!


🪝 示例 2:useFetch —— 简化数据请求

// hooks/useFetch.js
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    if (!url) return;

    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        setData(data);
        setError(null);
      })
      .catch(err => {
        setError(err.message);
        setData(null);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [url]); // URL 变化时重新请求

  return { data, loading, error };
}

✅ 使用方式:

function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

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

✨ 从此不用在每个组件里重复写 loading/error/data 三件套!


🪝 示例 3:useWindowSize —— 实时获取窗口大小

// hooks/useWindowSize.js
import { useState, useEffect } from 'react';

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; // { width, height }
}

✅ 使用方式:

function ResponsiveDisplay() {
  const { width } = useWindowSize();

  return (
    <div>
      当前屏幕宽度:{width}px
      {width < 768 ? <p>移动端</p> : <p>桌面端</p>}
    </div>
  );
}

四、命名规范与最佳实践 ✅

✅ 命名规则

  • 必须以 use 开头(如 useMousePosition, useForm
  • 文件名建议统一放在 /hooks 目录下
src/
├── components/
├── hooks/
│   ├── useLocalStorage.js
│   ├── useFetch.js
│   └── useWindowSize.js
└── App.js

✅ 设计原则

原则说明
只做一件事一个 Hook 专注一个功能(单一职责)
返回清晰结构返回对象或数组,便于解构使用
处理边界情况如空 URL、网络错误、SSR 环境(window 不存在)
支持默认值/配置参数让使用者更灵活

✅ 改进版 useFetch(支持配置)

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;

    fetch(url, options) // 支持传入 method, headers 等
      .then(...) // 同上
  }, [url, options.method, options.headers]); // 注意依赖项

  return { data, loading, error, refetch: () => {/* 可加重新请求功能 */} };
}

五、常见误区与避坑指南 ⚠️

❌ 错误 1:在条件中调用自定义 Hook

// ❌ 错误
if (isLoggedIn) {
  useFetch('/api/profile'); // 不能在条件中调用 Hook
}

✅ 正确做法:把逻辑放进 Hook 内部判断

function useProfileData(isLoggedIn) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (!isLoggedIn) return;
    fetch('/api/profile').then(...);
  }, [isLoggedIn]);

  return data;
}

❌ 错误 2:返回 JSX 或操作 DOM

// ❌ 错误:这不是组件!
function useAlertMessage() {
  return <div className="alert">警告!</div>; // ❌
}

✅ 正确:返回数据和函数,由组件决定如何渲染

function useAlert() {
  const [message, setMessage] = useState('');
  
  const showAlert = (msg) => setMessage(msg);
  const hideAlert = () => setMessage('');

  return { message, showAlert, hideAlert };
}

六、实战练习 🏋️‍♂️

✅ 练习 1:useInput —— 表单输入管理

目标:封装受控输入的通用逻辑

function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  const bind = {
    value,
    onChange: (e) => setValue(e.target.value),
  };
  return [value, bind, setValue];
}

// 使用
const [username, usernameBind] = useInput('');
<input placeholder="用户名" {...usernameBind} />

✅ 练习 2:useOnlineStatus —— 检测网络状态

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const goOnline = () => setIsOnline(true);
    const goOffline = () => setIsOnline(false);

    window.addEventListener('online', goOnline);
    window.addEventListener('offline', goOffline);

    return () => {
      window.removeEventListener('online', goOnline);
      window.removeEventListener('offline', goOffline);
    };
  }, []);

  return isOnline;
}

// 使用
function StatusIndicator() {
  const isOnline = useOnlineStatus();
  return <div>网络状态:{isOnline ? '在线' : '离线'}</div>;
}

✅ 总结:自定义 Hook 核心要点

要点说明
use 开头命名约定,让 React 识别
可调用其他 Hook这是它与普通函数的本质区别
封装逻辑而非 UI返回状态、函数、数据
支持参数和返回值灵活配置,易于使用
避免条件调用必须在顶层调用
可组合使用useFetch + useLocalStorage

🎯 下一步预告
你已经能“造轮子”了!接下来我们要进入工程化阶段:

➡️ React 状态管理进阶:Context + useReducer,迈向 Redux

是否继续?我将带你构建一个真正的全局状态管理系统。