自定义 Hooks 实战(上):封装技巧与 useLocalStorage

17 阅读2分钟

引言

在 React 开发中,Hooks 已经成为状态管理和逻辑复用的核心工具。除了 React 内置的 Hooks,自定义 Hooks 让我们能够将组件逻辑提取到可重用的函数中,实现更好的代码组织和复用。

今天我们来深入探讨自定义 Hooks 的封装技巧,并通过一个实用的 useLocalStorage Hook 来演示如何构建高质量的自定义 Hooks。

自定义 Hooks 的核心原则

1. 命名规范

自定义 Hooks 必须以 use 开头,这是 React 的硬性要求,也是代码可读性的保障:

// ✅ 正确
const useLocalStorage = (key, initialValue) => { ... }
const useFetch = (url) => { ... }
const useDebounce = (value, delay) => { ... }

// ❌ 错误
const localStorageHook = (key, initialValue) => { ... }
const fetchData = (url) => { ... }

2. 单一职责

每个自定义 Hooks 应该只负责一件事,保持逻辑清晰:

// ✅ 好的设计:每个 Hook 职责单一
const { user } = useAuth();
const { data } = useFetch('/api/user');
const { theme } = useTheme();

// ❌ 避免:一个 Hook 做太多事
const useEverything = () => {
  // 认证 + 数据获取 + 主题管理...
}

3. 返回值设计

返回清晰的接口,优先使用对象解构:

// ✅ 清晰的返回值
const { value, setValue, removeValue } = useLocalStorage('key', 'default');

// ✅ 多个返回值时用数组
const [count, setCount] = useCounter(0);

实战:useLocalStorage Hook

基础实现

useLocalStorage 是最常用的自定义 Hooks 之一,它让本地存储变得简单优雅:

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 读取初始值
  const readValue = () => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  };

  // 使用 lazy initialization
  const [storedValue, setStoredValue] = useState(readValue);

  // 监听其他标签页的变化
  useEffect(() => {
    const handleStorageChange = (event) => {
      if (event.key === key && event.newValue !== null) {
        try {
          setStoredValue(JSON.parse(event.newValue));
        } catch (error) {
          console.warn('Error parsing storage event:', error);
        }
      }
    };

    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  // 返回包装的 setter
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
        // 触发当前标签页的事件
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  const removeValue = () => {
    try {
      setStoredValue(initialValue);
      if (typeof window !== 'undefined') {
        window.localStorage.removeItem(key);
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error removing localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue, removeValue];
}

export default useLocalStorage;

使用示例

import useLocalStorage from './hooks/useLocalStorage';

function UserProfile() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [user, setUser] = useLocalStorage('user', null);

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <div className={`app ${theme}`}>
      <button onClick={toggleTheme}>
        切换到{theme === 'light' ? '深色' : '浅色'}模式
      </button>
      
      {user ? (
        <div>
          <p>欢迎,{user.name}!</p>
          <button onClick={logout}>退出登录</button>
        </div>
      ) : (
        <button onClick={() => setUser({ name: '访客' })}>
          模拟登录
        </button>
      )}
    </div>
  );
}

进阶:添加类型安全(TypeScript 版本)

import { useState, useEffect, Dispatch, SetStateAction } from 'react';

type SetValue<T> = Dispatch<SetStateAction<T>>;

function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>, () => void] {
  const readValue = (): T => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  };

  const [storedValue, setStoredValue] = useState<T>(readValue);

  useEffect(() => {
    const handleStorageChange = (event: StorageEvent) => {
      if (event.key === key && event.newValue !== null) {
        try {
          setStoredValue(JSON.parse(event.newValue) as T);
        } catch (error) {
          console.warn('Error parsing storage event:', error);
        }
      }
    };

    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  const setValue: SetValue<T> = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  const removeValue = () => {
    try {
      setStoredValue(initialValue);
      if (typeof window !== 'undefined') {
        window.localStorage.removeItem(key);
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error removing localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue, removeValue];
}

export default useLocalStorage;

封装技巧总结

  1. SSR 兼容:始终检查 window 是否存在
  2. 错误处理:用 try-catch 包裹 localStorage 操作
  3. 事件同步:监听 storage 事件实现多标签页同步
  4. 函数式更新:支持传入函数进行状态更新
  5. 类型安全:使用泛型提供完整的 TypeScript 支持

总结

自定义 Hooks 是 React 逻辑复用的强大工具。通过遵循命名规范、保持单一职责、设计清晰的返回值,我们可以构建出易于理解和维护的 Hooks。

useLocalStorage 作为一个经典案例,展示了如何处理浏览器 API、错误边界、跨标签页同步等实际问题。在下篇中,我们将继续探索 useFetchuseDebounceuseInterval 等更多实用的自定义 Hooks。