深入React自定义Hooks:从基础实践到高级封装

更新日期: 2025-06-11阅读: 49标签: Hooks

react开发中,随着业务复杂度提升,仅依靠内置Hooks往往难以满足需求。本文将系统性地介绍如何设计和封装高质量的自定义Hooks,帮助你提升代码复用性和开发效率。


一、自定义Hooks基础概念

1.1 什么是自定义Hooks

自定义Hooks本质上是一种逻辑复用机制,它允许你将组件逻辑提取到可重用的函数中。与工具函数不同,自定义Hooks:

  • 可以调用其他Hooks

  • 遵循use前缀命名约定

  • 保持React的声明式特性

1.2 设计原则

  1. 单一职责:每个Hook应只解决一个特定问题

  2. 明确依赖:清晰定义输入输出接口

  3. 性能优化:合理使用useMemo/useCallback避免不必要的计算

  4. 类型安全:为TypeScript项目提供完整的类型定义


二、实战:从简单到复杂的Hook封装

2.1 事件监听Hook优化版

原始版本存在handler更新不及时的问题,以下是改进实现:

import { useEffect, useRef } from 'react';

const useEventListener = <K extends keyof WindowEventMap>(
  eventType: K,
  handler: (event: WindowEventMap[K]) => void,
  element: Window | htmlElement = window,
  options?: boolean | AddEventListenerOptions
) => {
  const savedHandler = useRef(handler);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;

    const eventListener = (event: WindowEventMap[K]) => savedHandler.current(event);
    
    element.addEventListener(eventType, eventListener, options);
    
    return () => {
      element.removeEventListener(eventType, eventListener, options);
    };
  }, [eventType, element, options]);
};

// 使用示例
const WindowResizeDemo = () => {
  useEventListener('resize', (e) => {
    console.log('Window size:', window.innerWidth, window.innerHeight);
  });
  
  return <div>调整浏览器窗口大小查看控制台输出</div>;
};

关键改进

  1. 完整的TypeScript类型支持

  2. 支持更多事件目标元素

  3. 完善的options参数传递

  4. 添加了api可用性检查

2.2 增强版定时器Hook

针对复杂定时器场景,我们设计更强大的useInterval:

import { useEffect, useRef } from 'react';

const useAdvancedInterval = (
  callback: () => void,
  delay: number | null,
  options?: {
    immediate?: boolean;  // 是否立即执行
    maxTimes?: number;    // 最大执行次数
    onEnd?: () => void;   // 结束时回调
  }
) => {
  const savedCallback = useRef(callback);
  const timesRef = useRef(0);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return;

    let id: NodeJS.Timeout;
    const tick = () => {
      savedCallback.current();
      timesRef.current += 1;
      
      if (options?.maxTimes && timesRef.current >= options.maxTimes) {
        clearInterval(id);
        options.onEnd?.();
      }
    };

    if (options?.immediate) {
      tick(); // 立即执行一次
      id = setInterval(tick, delay);
    } else {
      id = setInterval(tick, delay);
    }

    return () => {
      clearInterval(id);
      timesRef.current = 0;
    };
  }, [delay, options?.immediate, options?.maxTimes, options?.onEnd]);
};

// 九宫格抽奖示例
const LotteryGrid = () => {
  const [position, setPosition] = useState(0);
  const [speed, setSpeed] = useState(500);
  const [isWinning, setIsWinning] = useState(false);
  
  useAdvancedInterval(
    () => {
      setPosition((prev) => (prev + 1) % 8);
      
      // 加速逻辑
      if (!isWinning && speed > 100) {
        setSpeed(s => s - 50);
      }
    },
    isWinning ? null : speed,
    { maxTimes: 20 }
  );

  // ...其他抽奖逻辑
};

高级特性

  1. 支持立即执行模式

  2. 可设置最大执行次数

  3. 结束回调通知

  4. 动态调整间隔时间


三、数据请求Hook的工业级实现

3.1 完整版useRequest实现

import { useState, useEffect, useCallback, useRef } from 'react';

type RequestOptions<T> = {
  manual?: boolean;
  defaultParams?: any[];
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
  formatResult?: (response: any) => T;
};

const useRequest = <T>(
  service: (...args: any[]) => Promise<T>,
  options: RequestOptions<T> = {}
) => {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(false);
  const paramsRef = useRef(options.defaultParams || []);

  const run = useCallback(async (...args: any[]) => {
    paramsRef.current = args;
    setLoading(true);
    try {
      let response = await service(...args);
      if (options.formatResult) {
        response = options.formatResult(response);
      }
      setData(response);
      options.onSuccess?.(response);
    } catch (err) {
      setError(err as Error);
      options.onError?.(err as Error);
    } finally {
      setLoading(false);
    }
  }, [service, options.formatResult, options.onSuccess, options.onError]);

  const refresh = useCallback(() => {
    return run(...paramsRef.current);
  }, [run]);

  useEffect(() => {
    if (!options.manual) {
      run(...(options.defaultParams || []));
    }
  }, [run, options.manual, options.defaultParams]);

  return {
    data,
    error,
    loading,
    run,
    refresh,
    mutate: setData, // 直接修改data
  };
};

// 使用示例
const UserInfo = ({ userId }) => {
  const { data: user, loading, error } = useRequest(
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    {
      formatResult: (res) => ({
        ...res,
        fullName: `${res.firstName} ${res.lastName}`,
      }),
      onError: (err) => {
        console.error('获取用户信息失败:', err);
        showToast('加载失败,请重试');
      }
    }
  );

  if (loading) return <Spinner />;
  if (error) return <ErrorDisplay />;
  
  return (
    <div>
      <h1>{user?.fullName}</h1>
      {/* 其他用户信息 */}
    </div>
  );
};

核心功能

  1. 自动/手动触发模式

  2. 结果格式化

  3. 生命周期钩子

  4. 数据突变能力

  5. 请求参数记忆

3.2 开源方案对比

特性自制useRequestSWRReact Queryahooks
自动缓存
请求去重
分页支持
乐观更新
依赖请求
轻量级
学习曲线

选型建议

  • 简单项目:自制Hook或ahooks

  • 数据密集型:React Query

  • 需要轻量方案:SWR


四、高级Hook模式

4.1 状态机Hook

const useStateMachine = <T extends string>(
  initialState: T,
  transitions: Record<T, Array<T>> // 状态转移规则
) => {
  const [state, setState] = useState(initialState);

  const transition = useCallback((newState: T) => {
    if (transitions[state].includes(newState)) {
      setState(newState);
    } else {
      console.warn(`Invalid transition from ${state} to ${newState}`);
    }
  }, [state]);

  return [state, transition] as const;
};

// 使用示例
const TrafficLight = () => {
  const [light, changeLight] = useStateMachine('red', {
    red: ['green'],
    green: ['yellow'],
    yellow: ['red'],
    // 红灯不能直接变黄灯
  });

  return (
    <div>
      <div style={{ color: light }}>当前: {light}</div>
      <button onClick={() => changeLight('green')}>变绿灯</button>
      {/* 其他按钮 */}
    </div>
  );
};

4.2 防抖/节流Hook

const useDebounce = <T extends any[]>(
  callback: (...args: T) => void,
  delay: number
) => {
  const timerRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    };
  }, []);

  return useCallback((...args: T) => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
    
    timerRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);
};

// 使用示例
const SearchBox = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleSearch = useDebounce(async (value) => {
    const res = await fetch(`/api/search?q=${value}`);
    setResults(await res.json());
  }, 500);

  return (
    <div>
      <input 
        value={query}
        onChange={(e) => {
          setQuery(e.target.value);
          handleSearch(e.target.value);
        }}
      />
      {/* 显示结果 */}
    </div>
  );
};


五、测试与调试自定义Hooks

5.1 测试策略

import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

5.2 调试技巧

使用useDebugValue展示Hook内部状态

const useToggle = (initialState = false) => {
const [state, setState] = useState(initialState);
useDebugValue(state ? 'On' : 'Off');
// ...
};

React DevTools查看Hook依赖关系


六、Hook集合推荐

  1. ahooks (阿里出品)

    • useRequest: 强大的异步管理

    • useAntdTable: 与Ant Design深度集成

    • useDrag/useDrop: 拖拽功能

  2. react-use (社区流行)

    • usePrevious: 获取之前的值

    • useClickAway: 点击外部区域触发

    • useLocalStorage: 本地存储同步

  3. Redux Toolkit Hooks

    • useSelector/useDispatch: Redux集成

    • useStore: 访问整个store


七、挑战与解决方案

挑战1:Hook执行顺序必须一致

  • 解决方案:不要在条件/循环中使用Hook

  • 错误示例:

    if (condition) {
      useEffect(() => {...}); // ❌
    }

挑战2:闭包陷阱

  • 解决方案:使用ref保存最新值或使用函数式更新

    const [count, setCount] = useState(0);
    
    // 错误方式
    useEffect(() => {
      const timer = setInterval(() => {
        setCount(count + 1); // 闭包问题
      }, 1000);
      return () => clearInterval(timer);
    }, []);
    
    // 正确方式
    useEffect(() => {
      const timer = setInterval(() => {
        setCount(prev => prev + 1); // 函数式更新
      }, 1000);
      return () => clearInterval(timer);
    }, []);

挑战3:性能优化

  • 解决方案:合理使用useMemo/useCallback

    const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    const stableCallback = useCallback(() => doSomething(a, b), [a, b]);


八、练习:实现useSwitch

const useSwitch = (initialValue = false) => {
  const [state, setState] = useState(initialValue);
  
  const toggle = useCallback(() => {
    setState(prev => !prev);
  }, []);
  
  return [state, toggle] as const;
};

// 使用示例
const ToggleButton = () => {
  const [on, toggle] = useSwitch(false);
  
  return (
    <button onClick={toggle}>
      {on ? 'ON' : 'OFF'}
    </button>
  );
};

通过本文的学习,你应该已经掌握了自定义Hooks从设计到实现的完整知识体系。记住,好的Hook应该像乐高积木一样,既独立完整又能与其他部分完美配合。在实际项目中,不断提炼和优化你的Hooks,它们将成为你React开发中的强大武器。

链接: https://fly63.com/article/detial/12778

React将引入Hooks,你怎么看?

近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?

精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。

使用react hooks实现自己的context-redux

我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理

useEffect Hook 是如何工作的?

使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。

React Hooks 你真的用对了吗?

从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考

如何用 Hooks 来实现 React Class Component 写法?

Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的

React-Hooks

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

React Hooks与setInterval

Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?

React Hooks 底层解析[译]

对于 React 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例

React Hooks实践

9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!