一、为什么需要自定义Hook

在React开发中,我们经常会遇到一些重复的业务逻辑,比如表单处理、数据请求、状态管理等。如果每次都重新写一遍,不仅浪费时间,还容易出错。这时候,自定义Hook就派上用场了。

自定义Hook的本质是封装可复用的逻辑,让代码更简洁、更易于维护。它遵循React Hook的规则,可以调用其他Hook(比如useStateuseEffect),但本身只是一个普通的JavaScript函数。

举个例子,假设我们有一个需求:在多个组件里监听窗口大小变化。如果不封装,每个组件都要写一遍useEffectuseState,代码会显得很冗余。但如果封装成自定义Hook,就可以像下面这样轻松复用:

// 技术栈:React + TypeScript  
import { useState, useEffect } from 'react';  

/**
 * 自定义Hook:监听窗口大小变化  
 * @returns { width: number, height: number } 当前窗口的宽高  
 */  
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 ResponsiveComponent() {  
  const { width, height } = useWindowSize();  
  return <div>当前窗口大小:{width} x {height}</div>;  
}  

这个例子展示了自定义Hook的核心优势:逻辑复用。我们可以把任何重复的逻辑抽离出来,变成一个独立的Hook,然后在多个组件里调用。

二、如何设计一个好的自定义Hook

自定义Hook虽然灵活,但设计不当可能会导致代码难以维护。以下是几个关键原则:

1. 单一职责

一个Hook应该只做一件事。比如useFetch负责数据请求,useLocalStorage负责本地存储操作,而不是把所有逻辑都塞进一个Hook里。

2. 清晰的输入输出

Hook的参数和返回值要明确,避免过于复杂的结构。例如:

// 技术栈:React + TypeScript  
import { useState, useEffect } from 'react';  

/**
 * 自定义Hook:模拟数据请求  
 * @param url 请求地址  
 * @returns { data: T | null, loading: boolean, error: Error | null }  
 */  
function useFetch<T>(url: string) {  
  const [data, setData] = useState<T | null>(null);  
  const [loading, setLoading] = useState(true);  
  const [error, setError] = useState<Error | null>(null);  

  useEffect(() => {  
    fetch(url)  
      .then((res) => res.json())  
      .then((data) => setData(data))  
      .catch((err) => setError(err))  
      .finally(() => setLoading(false));  
  }, [url]);  

  return { data, loading, error };  
}  

// 使用示例  
function UserList() {  
  const { data, loading, error } = useFetch<User[]>('/api/users');  
  if (loading) return <div>加载中...</div>;  
  if (error) return <div>加载失败:{error.message}</div>;  
  return <ul>{data?.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;  
}  

3. 支持依赖项更新

如果Hook内部使用了useEffect,记得正确处理依赖项,避免不必要的重复执行。

三、常见业务场景的Hook封装

1. 表单处理Hook

表单是前端开发中最常见的场景之一,我们可以封装一个useForm Hook来简化状态管理:

// 技术栈:React + TypeScript  
import { useState } from 'react';  

/**
 * 自定义Hook:表单状态管理  
 * @param initialValue 表单初始值  
 * @returns { values: T, handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void, resetForm: () => void }  
 */  
function useForm<T>(initialValue: T) {  
  const [values, setValues] = useState<T>(initialValue);  

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {  
    const { name, value } = e.target;  
    setValues({ ...values, [name]: value });  
  };  

  const resetForm = () => setValues(initialValue);  

  return { values, handleChange, resetForm };  
}  

// 使用示例  
function LoginForm() {  
  const { values, handleChange, resetForm } = useForm({  
    username: '',  
    password: '',  
  });  

  const handleSubmit = (e: React.FormEvent) => {  
    e.preventDefault();  
    console.log('提交表单:', values);  
  };  

  return (  
    <form onSubmit={handleSubmit}>  
      <input name="username" value={values.username} onChange={handleChange} />  
      <input name="password" type="password" value={values.password} onChange={handleChange} />  
      <button type="submit">登录</button>  
      <button type="button" onClick={resetForm}>重置</button>  
    </form>  
  );  
}  

2. 定时器Hook

定时任务也是常见的需求,我们可以封装一个useInterval Hook:

// 技术栈:React + TypeScript  
import { useEffect, useRef } from 'react';  

/**
 * 自定义Hook:定时执行任务  
 * @param callback 回调函数  
 * @param delay 间隔时间(毫秒)  
 */  
function useInterval(callback: () => void, delay: number | null) {  
  const savedCallback = useRef<() => void>();  

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

  useEffect(() => {  
    if (delay === null) return;  
    const tick = () => savedCallback.current?.();  
    const id = setInterval(tick, delay);  
    return () => clearInterval(id);  
  }, [delay]);  
}  

// 使用示例  
function Timer() {  
  const [count, setCount] = useState(0);  
  useInterval(() => setCount(count + 1), 1000);  
  return <div>计时:{count}秒</div>;  
}  

四、注意事项与最佳实践

  1. 命名规范
    自定义Hook的名称应该以use开头,比如useFetchuseWindowSize,这样React才能识别它是一个Hook。

  2. 避免条件调用
    Hook的调用顺序必须保持一致,不能在条件语句或循环中调用Hook。

  3. 性能优化
    如果Hook内部有计算量大的逻辑,可以使用useMemouseCallback优化。

  4. 测试覆盖
    自定义Hook应该和普通函数一样进行单元测试,确保逻辑正确。

五、总结

自定义Hook是React中非常强大的功能,它能让我们把复杂的业务逻辑封装成可复用的单元,提升代码的可维护性和开发效率。无论是表单处理、数据请求,还是定时任务,都可以通过自定义Hook优雅地实现。

当然,Hook不是万能的,过度封装反而会让代码难以理解。在实际开发中,我们应该根据业务需求合理设计Hook,遵循单一职责原则,并注意性能优化和测试覆盖。