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。它用到了 useStateuseEffect,所以它自己也是个 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 查看我的更多作品。欢迎大家来观看!

相关推荐
冬奇Lab1 小时前
让 AI Agent 更可靠:Harness Engineering 与多 Agent 系统工程实践
人工智能·llm·agent
想你依然心痛1 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“文思智脑“——PC端AI智能体沉浸式智能写作工作台
人工智能·ar·harmonyos·ai写作
冬奇Lab1 小时前
一天一个开源项目(第108篇):Andrej Karpathy Skills - 用一个 CLAUDE.md 文件修复 LLM 编码的四个顽疾
人工智能·开源·资讯
涛声依旧-底层原理研究所1 小时前
残差连接与层归一化通俗易懂的详解
人工智能·python·神经网络·transformer
fantasy_arch2 小时前
pytorch人脸匹配模型
人工智能·pytorch·python
科技那些事儿2 小时前
实时洞察,视觉赋能:国内情绪识别API公司推荐及计算机视觉流派深度解析
人工智能·计算机视觉
XinZong2 小时前
OpenClaw 实现双重心跳(Heartbeat)+ clawreach虾聊项目实现
javascript
德思特2 小时前
从 Dify 配置页理解 RAG 的重要参数
java·人工智能·llm·dify·rag