React 从入门到生产(四):自定义 Hook

创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0


为什么需要自定义 Hook

假设你在三个不同的页面都要做一个功能:用户输入搜索词后,等 300ms 没动静了才发请求(防抖)。

你可以在每个页面都写一遍:

jsx 复制代码
function SearchPage1() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (query) fetchResults(query).then(setResults);
    }, 300);
    return () => clearTimeout(timeout);
  }, [query]);
  // ... 这段代码要复制三份!
}

这就是典型的代码重复。防抖逻辑本身是独立的,完全可以抽出来。而且当产品说"改成 500ms"时,你只要改一个地方。

自定义 Hook 就是为这个场景设计的------把可复用的逻辑封装成函数,这个函数内部可以用其他 Hook。


自定义 Hook 的基本结构

自定义 Hook 本质上就是一个普通的 JavaScript 函数,函数名以 use 开头,内部可以调用其他 Hook。

javascript 复制代码
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timeout);
  }, [value, delay]);

  return debouncedValue;
}

这就是一个 Hook。它用到了 useState 和 useEffect,所以它自己也是个 Hook(React 的规则:只有 Hook 才能调用其他 Hook)。

用法

jsx 复制代码
function SearchPage() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) fetchResults(debouncedQuery);
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

现在防抖逻辑只在一处定义了,三个页面都可以用。


经典案例:useDebounce

继续深化这个案例,加上更多防抖的变体:

javascript 复制代码
function useDebouncedValue(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 防抖版表单:用户停止输入后才更新
function useDebouncedForm(initialValues, delay = 300) {
  const [values, setValues] = useState(initialValues);

  // 为每个字段单独防抖
  const debouncedValues = {};
  for (const key in values) {
    debouncedValues[key] = useDebouncedValue(values[key], delay);
  }

  function handleChange(key, value) {
    setValues((prev) => ({ ...prev, [key]: value }));
  }

  return { values, debouncedValues, handleChange };
}

经典案例:useFetch

数据获取是另一个重复高频的��景:

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

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      try {
        setLoading(true);
        setError(null);

        const res = await fetch(url, {
          ...options,
          signal: controller.signal
        });

        if (!res.ok) throw new Error(`HTTP ${res.status}`);

        const json = await res.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    return () => controller.abort();
  }, [url, JSON.stringify(options)]);

  return { data, loading, error, refetch: () => /* ... */ };
}

用法变得极其简单

jsx 复制代码
function UserList() {
  const { data, loading, error } = useFetch('/api/users');

  if (loading) return <Spinner />;
  if (error) return <Error msg={error} />;

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

一行代码替代了 30 行重复的请求/加载/错误逻辑。这就是 Hook 的价值。


经典案例:useLocalStorage

把数据存进浏览器本地存储,同时保持和 React 状态的同步:

javascript 复制代码
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`读取 localStorage key "${key}" 失败:`, error);
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`写入 localStorage key "${key}" 失败:`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

用法:记住用户的偏好

jsx 复制代码
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark');
  const [language, setLanguage] = useLocalStorage('language', 'zh-CN');

  // 用户刷新页面后,主题和语言自动恢复
  return <ThemeProvider theme={theme}>...</ThemeProvider>;
}

Hook 组合:更复杂的逻辑

你可以把多个自定义 Hook 组合在一起,形成更强大的逻辑:

javascript 复制代码
// 一个组合 Hook:用户搜索 + 防抖 + 缓存
function useSearch(query, options = {}) {
  const { baseUrl = '/api/search', delay = 300, cache = true } = options;
  
  const debouncedQuery = useDebounce(query, delay);
  const cacheKey = `${baseUrl}:${debouncedQuery}`;
  const [cachedData, setCachedData] = useLocalStorage(`search-cache`, {});
  const [freshData, setFreshData] = useState(null);

  // 优先用缓存
  const data = cache && cachedData[cacheKey] ? cachedData[cacheKey] : freshData;

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

    // 检查缓存
    const cached = cachedData[cacheKey];
    if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
      // 5 分钟内的缓存直接用
      return;
    }

    // 发新请求
    fetch(`${baseUrl}?q=${encodeURIComponent(debouncedQuery)}`)
      .then((r) => r.json())
      .then((d) => {
        setFreshData(d);
        // 更新缓存
        setCachedData((prev) => ({
          ...prev,
          [cacheKey]: { ...d, timestamp: Date.now() }
        }));
      });
  }, [debouncedQuery, baseUrl]);

  return { data, isLoading: !debouncedQuery || !data };
}

这就是所谓的「管道式」架构------每个 Hook 只做一件事,组合起来就拥有了完整功能。


测试自定义 Hook

自定义 Hook 的测试需要一点特殊处理------React Testing Library 专门为 Hook 提供了 renderHook:

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

test('useDebounce 应该延迟返回新值', () => {
  const { result, rerender } = renderHook(
    ({ value, delay }) => useDebounce(value, delay),
    { initialProps: { value: 'hello', delay: 300 } }
  );

  expect(result.current).toBe('hello');

  // 修改值
  rerender({ value: 'world', delay: 300 });
  expect(result.current).toBe('hello');  // 还没到 300ms

  // 等 300ms
  jest.advanceTimersByTime(300);
  expect(result.current).toBe('world');
});

本章小结

概念 一句话总结
自定义 Hook 以 use 开头的函数,内部可调用其他 Hook
useDebounce 延迟更新值,常用于搜索输入
useFetch 封装请求逻辑,一行代码搞定数据获取
useLocalStorage 持久化状态到浏览器本地存储
Hook 组合 把多个简单 Hook 组合成复杂逻辑

自定义 Hook 把可复用的逻辑抽离出来------这是 React 应用架构的核心技能。下一章我们聊 状态管理------当组件树越来越深时,怎么让状态在任意位置都能访问。


📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top

📖 本章是「React 从入门到生产 」系列的第 4 章。上一章:副作用与数据获取 | 下一章:状态管理选型

🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!

相关推荐
2501_9381400713 小时前
做内容很省心,2026好用AI笔记,支持抖音小红书视频无水印保存
人工智能
sell-cloud16 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
java1234_小锋16 小时前
【技术专题】HTML5 网页技术 - HTML5 Canvas 基础以及动画入门
前端·html·html5
Navicat中国17 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI17 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区18 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
知了学历孙老师19 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
XCDoris19 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy20 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V20 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai