自定义 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。

相关推荐
Aphasia3117 小时前
手写KeepAlive组件
前端·react.js·面试
whatever who cares12 小时前
大型 React 项目的文件结构
前端·react.js·前端框架
假如让我当三天老蒯12 小时前
useCallback 详细解释(从原理到用法)(自学用)
前端·react.js
Vu46113 小时前
nextjs的图片和文字优化
react.js
gogoing15 小时前
React Hooks 完整指南
react.js
假如让我当三天老蒯17 小时前
State和Props区别和左右(自学用)
前端·react.js
夜雪闻竹18 小时前
React Query + REST API 最佳实践
前端·react.js·前端框架
戈德斯文19 小时前
我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境
react.js·canvas·next.js
vim怎么退出2 天前
Dive into React——Hooks 原理
react.js·源码阅读