Skip to content
第 152 / 250 章前端⏱ 10 分钟阅读

第 152 章:自定义 Hooks

学习目标

  • 理解自定义 Hook 的规则
  • 学会封装常用逻辑
  • 掌握 5 个经典自定义 Hook
  • 养成"逻辑抽 Hook"的习惯

一、自定义 Hook 是什么

use 开头的函数,内部可以调用其他 Hook,用于复用状态逻辑

tsx
// 一个最简单的自定义 Hook
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle] as const;
}

// 使用
function MyComponent() {
  const [open, toggleOpen] = useToggle(false);
  return <button onClick={toggleOpen}>{open ? '关' : '开'}</button>;
}

二、规则

2.1 命名以 use 开头

tsx
// ✅ 合法
function useLocalStorage() { }
function useFetch() { }

// ❌ 不算 Hook,不会做规则检查
function getData() { }

2.2 只能在顶层调用

tsx
// ❌ 不要在条件里
function useBad() {
  if (something) {
    const [x] = useState(0);   // 错
  }
}

// ✅ 顶层
function useGood() {
  const [x] = useState(0);
  if (something) { /* 用 x */ }
}

三、5 个经典自定义 Hook

3.1 useLocalStorage

tsx
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initial;
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initial;
    } catch {
      return initial;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {}
  }, [key, value]);

  return [value, setValue] as const;
}

// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');

3.2 useFetch

tsx
interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string) {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

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

    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name === 'AbortError') return;
        setState({ data: null, loading: false, error: err });
      });

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

  return state;
}

// 使用
const { data, loading, error } = useFetch<User[]>('/api/users');

3.3 useDebounce

tsx
function useDebounce<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

// 使用
const [query, setQuery] = useState('');
const debounced = useDebounce(query, 500);
useEffect(() => {
  if (debounced) search(debounced);
}, [debounced]);

3.4 usePrevious

tsx
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

3.5 useToggle

tsx
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse } as const;
}

四、useOnline(网络状态)

tsx
function useOnline() {
  const [online, setOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  );

  useEffect(() => {
    const onOnline = () => setOnline(true);
    const onOffline = () => setOnline(false);
    window.addEventListener('online', onOnline);
    window.addEventListener('offline', onOffline);
    return () => {
      window.removeEventListener('online', onOnline);
      window.removeEventListener('offline', onOffline);
    };
  }, []);

  return online;
}

五、useMediaQuery(响应式)

tsx
function useMediaQuery(query: string) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia(query);
    setMatches(mq.matches);

    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// 使用
const isMobile = useMediaQuery('(max-width: 768px)');

六、组合 Hook

tsx
function useUser(id: number) {
  const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
  const isAdmin = data?.role === 'admin';
  return { user: data, loading, error, isAdmin };
}

七、本章小结

概念关键
自定义 Hookuse 开头的函数
用途复用状态逻辑
规则顶层调用、只在 React 中用
常见useLocalStorage / useFetch / useDebounce

动手练习

  1. 写 useLocalStorage
  2. 写 useFetch(带 loading/error)
  3. 写 useDebounce + 搜索框
  4. 写 usePrevious
  5. 写 useScrollPosition(滚动监听)

推荐阅读


下一章:第 153 章:表单与受控组件

本站基于 VitePress 构建 · 由 Codebook 团队维护