Skip to content
📖预计阅读时长:0 分钟字数:0

React 自定义 Hooks

进阶篇第 2 章:学习设计和实现自定义 Hooks,提升代码复用性。

返回 React学习路线 | 上一章 React-07-Hooks进阶 | 下一章 React-09-状态管理


一、自定义 Hook 基础

1.1 什么是自定义 Hook

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

jsx
// 自定义 Hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

// 使用
function Counter() {
  const { count, increment, decrement, reset } = useCounter(10);

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

1.2 设计原则

  1. use 开头:遵循 React 约定
  2. 单一职责:一个 Hook 只做一件事
  3. 返回值一致:考虑返回数组或对象
  4. 参数设计:提供合理的默认值

二、常用自定义 Hooks

2.1 useToggle

jsx
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 };
}

// 使用
const { value: isOpen, toggle, setFalse: close } = useToggle();

2.2 useLocalStorage

jsx
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      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.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

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

2.3 useFetch

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

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Network error');
        const json = await response.json();
        if (!cancelled) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

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

2.4 useDebounce

jsx
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 [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      // 执行搜索
      search(debouncedQuery);
    }
  }, [debouncedQuery]);

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

2.5 useClickOutside

jsx
function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// 使用
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef(null);

  useClickOutside(ref, () => setIsOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && <div className="dropdown-menu">...</div>}
    </div>
  );
}

2.6 useMediaQuery

jsx
function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => 
    window.matchMedia(query).matches
  );

  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    const handler = (e) => setMatches(e.matches);

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// 使用
function Component() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');

  return isMobile ? <MobileLayout /> : <DesktopLayout />;
}

三、Hook 组合模式

3.1 组合多个 Hooks

jsx
function useUser(userId) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  const [isFollowing, setIsFollowing] = useLocalStorage(`following-${userId}`, false);

  const toggleFollow = useCallback(() => {
    setIsFollowing(prev => !prev);
  }, [setIsFollowing]);

  return {
    user,
    loading,
    error,
    isFollowing,
    toggleFollow,
  };
}

3.2 Hook 工厂模式

jsx
function createResourceHook(resourceName) {
  return function useResource(id) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      fetch(`/api/${resourceName}/${id}`)
        .then(res => res.json())
        .then(data => {
          setData(data);
          setLoading(false);
        });
    }, [id]);

    return { data, loading };
  };
}

// 创建特定资源的 Hook
const useUser = createResourceHook('users');
const usePost = createResourceHook('posts');

四、推荐的 Hook 库

特点
ahooks阿里出品,功能全面
react-use社区流行,Hook 丰富
usehooks-tsTypeScript 优先

五、本章小结

常用自定义 Hooks 速查

Hook用途
useToggle布尔值切换
useLocalStorage本地存储
useFetch数据请求
useDebounce防抖
useClickOutside点击外部检测
useMediaQuery媒体查询

下一步


#React #Hooks #自定义Hook #代码复用