告别重复代码!React自定义Hook让逻辑复用如此简单

你是不是经常在多个组件里写同样的逻辑?比如监听窗口大小变化,或者跟踪鼠标位置?

每次都要写一遍useEffect添加监听,还要在卸载时清理事件... 这样的重复代码不仅浪费时间,还容易出错。

今天我要分享的React自定义Hook,就是解决这个痛点的神器!它能让你像搭积木一样封装和复用组件逻辑,代码整洁度直接提升一个level!

什么是自定义Hook?先看一个实际例子

让我直接用一个获取窗口大小的例子来展示自定义Hook的魅力。

没有自定义Hook之前,你可能这样写:

jsx 复制代码
import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    
    // 清理函数:组件卸载时移除监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组表示只在组件挂载时执行一次

  return (
    <div>
      当前窗口大小:{windowSize.width} x {windowSize.height}
    </div>
  );
}

这样写没问题,但如果另一个组件也需要窗口大小呢?难道要复制粘贴一遍?

使用自定义Hook后,代码变得超简洁:

jsx 复制代码
import React from 'react';

// 自定义Hook:useWindowSize
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return windowSize; // 返回窗口大小状态
}

// 在组件中使用
function MyComponent() {
  const windowSize = useWindowSize(); // 一行代码搞定!

  return (
    <div>
      当前窗口大小:{windowSize.width} x {windowSize.height}
    </div>
  );
}

看到差别了吗?自定义Hook把复杂的逻辑封装起来,使用时只需要一行代码!这才是真正的"写一次,到处用"。

再来看一个跟踪鼠标位置的自定义Hook

鼠标位置跟踪是另一个常见需求,比如实现拖拽效果、鼠标悬停提示等。

jsx 复制代码
import { useState, useEffect } from 'react';

// 自定义Hook:useMousePosition
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };

    // 监听鼠标移动事件
    window.addEventListener('mousemove', handleMouseMove);
    
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // 空依赖数组确保effect只运行一次

  return position; // 返回鼠标位置
}

// 使用示例
function CursorTracker() {
  const mousePos = useMousePosition();
  
  return (
    <div>
      鼠标当前位置:({mousePos.x}, {mousePos.y})
    </div>
  );
}

// 另一个组件也可以复用同样的逻辑
function Tooltip() {
  const { x, y } = useMousePosition();
  
  return (
    <div style={{ position: 'absolute', left: x + 10, top: y + 10 }}>
      我是跟随鼠标的提示框!
    </div>
  );
}

这两个例子展示了自定义Hook的核心价值:把状态逻辑从组件中提取出来,实现真正的逻辑复用

自定义Hook的命名约定:为什么一定要用use开头?

你可能注意到了,所有自定义Hook都以use开头,比如useWindowSizeuseMousePosition

这可不是随便起的名字,而是React官方强制要求的约定!原因有三:

1. 让React工具能识别Hook React DevTools等调试工具依赖use前缀来识别哪些函数是Hook,这样在调试时能正确显示Hook的状态和调用顺序。

2. 避免违反Hook规则 ESLint的React Hook插件会检查以use开头的函数,确保它们遵守Hook的规则。如果你不用use开头,这些重要的静态检查就失效了。

3. 提高代码可读性 看到use开头,其他开发者(包括未来的你)立即知道这是一个自定义Hook,而不是普通函数。

jsx 复制代码
// ✅ 正确:use开头,明确这是自定义Hook
function useLocalStorage(key, initialValue) {
  // ... Hook逻辑
}

// ❌ 错误:没有use前缀,React工具无法识别
function getLocalStorage(key, initialValue) {
  // 这里如果用了useState等Hook,ESLint会报错
}

记住这个简单的规则:自定义Hook的名字必须以use开头,后面跟描述性的名称,使用驼峰命名法。

实战:组合多个自定义Hook实现复杂功能

真正的威力在于,你可以像搭积木一样组合多个自定义Hook!

比如,我们要实现一个响应式的组件,在桌面端显示详细内容,在移动端显示简化版,并且根据鼠标位置改变样式:

jsx 复制代码
// 组合使用之前定义的两个Hook
function ResponsiveComponent() {
  const windowSize = useWindowSize();
  const mousePosition = useMousePosition();
  
  const isMobile = windowSize.width < 768;
  const isNearTop = mousePosition.y < 100;
  
  return (
    <div style={{ 
      background: isNearTop ? '#f0f0f0' : '#ffffff',
      padding: isMobile ? '10px' : '20px'
    }}>
      {isMobile ? (
        <div>移动端简化版</div>
      ) : (
        <div>
          <h1>桌面端详细内容</h1>
          <p>鼠标位置:({mousePosition.x}, {mousePosition.y})</p>
          <p>窗口大小:{windowSize.width} x {windowSize.height}</p>
        </div>
      )}
    </div>
  );
}

这种组合能力让代码既简洁又强大,每个Hook专注做好一件事,组件只关心如何组合这些功能。

必须避开的坑:Hook的三大规则

自定义Hook虽然强大,但必须遵守React Hook的基本规则,否则会掉进各种坑里。

规则1:只在最顶层调用Hook

不要在循环、条件或嵌套函数中调用Hook

jsx 复制代码
// ❌ 错误示例:在条件语句中调用Hook
function BadComponent({ shouldTrack }) {
  if (shouldTrack) {
    const position = useMousePosition(); // 这样写会出错!
  }
  
  return <div>错误示例</div>;
}

// ✅ 正确写法:无条件调用,在effect中控制逻辑
function GoodComponent({ shouldTrack }) {
  const position = useMousePosition(); // 始终调用
  
  useEffect(() => {
    if (shouldTrack) {
      // 在这里使用position做某些事情
      console.log('跟踪位置:', position);
    }
  }, [shouldTrack, position]);
  
  return <div>正确示例</div>;
}

为什么有这个规则? React依赖Hook的调用顺序来正确管理状态。如果条件不同导致Hook调用顺序变化,状态就会乱套。

规则2:只在React函数中调用Hook

在React函数组件或自定义Hook中调用,不要在普通JavaScript函数中调用

jsx 复制代码
// ❌ 错误:在普通函数中调用Hook
function regularFunction() {
  const [value, setValue] = useState(''); // 报错!
}

// ✅ 正确:在组件或自定义Hook中调用
function MyComponent() {
  const [value, setValue] = useState(''); // 正确
}

function useCustomHook() {
  const [value, setValue] = useState(''); // 正确(自定义Hook也是Hook)
}

规则3:自定义Hook必须返回需要的内容

根据需要返回状态、函数或其他值,保持接口清晰

jsx 复制代码
// ✅ 好的设计:返回需要的状态和操作方法
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = () => setValue(!value);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);
  
  return [value, toggle, setTrue, setFalse]; // 返回数组方便重命名
}

// 使用时的灵活性
function MyComponent() {
  const [isOpen, toggleOpen, open, close] = useToggle(false);
  
  return (
    <div>
      <button onClick={toggleOpen}>切换</button>
      <button onClick={open}>打开</button>
      <button onClick={close}>关闭</button>
      {isOpen && <div>内容</div>}
    </div>
  );
}

进阶技巧:带参数的自定义Hook

自定义Hook可以接受参数,根据参数不同返回不同的逻辑。

比如,一个增强版的localStorage Hook:

jsx 复制代码
function useLocalStorage(key, initialValue) {
  // 从localStorage读取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  // 返回的setter函数会同时更新状态和localStorage
  const setValue = (value) => {
    try {
      // 允许value是函数,就像useState一样
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

// 使用示例
function UserSettings() {
  // 不同的key创建不同的存储空间
  const [username, setUsername] = useLocalStorage('username', '');
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <div>
      <input 
        value={username} 
        onChange={e => setUsername(e.target.value)} 
        placeholder="输入用户名"
      />
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
    </div>
  );
}

真实场景:封装数据获取逻辑

数据获取是自定义Hook的经典应用场景。看看如何封装一个通用的数据获取Hook:

jsx 复制代码
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP错误: ${response.status}`);
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]); // url变化时重新获取

  return { data, loading, error };
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  if (!user) return <div>用户不存在</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

这个useApi Hook处理了加载状态、错误处理、数据缓存等繁琐细节,让组件代码变得异常简洁。

性能优化:避免不必要的重新渲染

自定义Hook也可能引入性能问题,特别是当返回对象或数组时:

jsx 复制代码
// ❌ 可能引起性能问题:每次渲染都返回新对象
function useUserInfo(userId) {
  const [user, setUser] = useState(null);
  
  // 每次渲染都返回新对象,即使user没变
  return {
    user,
    isAdmin: user?.role === 'admin',
    canEdit: user?.permissions?.includes('edit')
  };
}

// ✅ 优化版本:使用useMemo缓存计算结果
function useUserInfoOptimized(userId) {
  const [user, setUser] = useState(null);
  
  const userInfo = useMemo(() => ({
    user,
    isAdmin: user?.role === 'admin',
    canEdit: user?.permissions?.includes('edit')
  }), [user]); // 只有user变化时才重新计算
  
  return userInfo;
}

测试自定义Hook:确保可靠性

自定义Hook也需要测试!推荐使用@testing-library/react-hooks

jsx 复制代码
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';

test('useCounter应该正确增加计数', () => {
  const { result } = renderHook(() => useCounter(0));
  
  // 初始值应该是0
  expect(result.current.count).toBe(0);
  
  // 测试increment
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

总结:什么时候该用自定义Hook?

经过这么多例子,你可能想问:到底什么时候应该创建自定义Hook?

适合使用自定义Hook的场景:

  • 多个组件共享相同的状态逻辑
  • 复杂的useEffect逻辑需要封装
  • 想要分离关注点,让组件更专注于UI
  • 需要复用数据获取、事件监听等副作用逻辑

不适合的情况:

  • 逻辑只在一个组件中使用,且不太可能复用
  • 逻辑非常简单,封装反而增加复杂度
  • 只是简单的工具函数,不涉及React状态或生命周期

自定义Hook是React函数组件的终极武器,它让逻辑复用变得前所未有的简单。从今天开始,试着把你项目中的重复逻辑提取成自定义Hook吧!

相关推荐
qwy7152292581632 小时前
Vue中的Provide/Inject如何实现动态数据
前端·javascript·vue.js
yoyoma2 小时前
react-infinite-scroll-component 使用注意事项
前端
快乐是一切2 小时前
PDF文件的交叉引用表(xref)与 trailer
前端
Never_Satisfied2 小时前
在JavaScript / HTML中,让<audio>元素中的多个<source>标签连续播放
开发语言·javascript·html
emma羊羊2 小时前
【CSRF】防御
前端·网络安全·csrf
Paddy哥3 小时前
html调起exe程序
前端·html
emma羊羊3 小时前
【CSRF】跨站请求伪造
前端·网络安全·csrf
患得患失9493 小时前
【ThreeJs】【HTML载入】Three.js 中的 CSS2DRenderer 与 CSS3DRenderer 全面解析
javascript·html·css3
折翼的恶魔3 小时前
HTML基本标签二:
前端·html