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

引言

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

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

自定义 Hooks 的核心原则

1. 命名规范

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

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

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

2. 单一职责

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

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

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

3. 返回值设计

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

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

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

实战:useLocalStorage Hook

基础实现

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

javascript 复制代码
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;

使用示例

javascript 复制代码
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 版本)

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。

相关推荐
我是大卫9 小时前
React源码解析-第一部分:宏观架构
前端·react.js·源码
我是大卫9 小时前
React源码解析-第二部分:更新与调度
前端·react.js·源码
用户2986985301414 小时前
如何在 JavaScript 和 React 中下载/导出 Excel 文件
前端·javascript·react.js
随风一样自由15 小时前
【前端+React+缓存竞态】导航切换缓存刷新机制
前端·react.js·缓存
binbin_5216 小时前
React Native 鸿蒙环境搭建完整实战:从 RN 工程到 RNOH 运行
深度学习·react native·react.js·harmonyos
kgduu16 小时前
react-redux学习笔记
笔记·学习·react.js
YFF菲菲兔1 天前
其他 Hooks 解析
react.js
想你依然心痛2 天前
AtomCode 在前端开发中的实战体验:React + TypeScript 项目开发实录
前端·react.js·typescript
前端炒粉2 天前
个人简历面经总结二
前端·网络·vue.js·react.js·面试
谢尔登2 天前
【React】 状态管理方案
前端·react.js·前端框架