自定义Hook炼金术:从数据请求到拖拽插件的封装艺术

概念意义与作用

什么是自定义Hook?

自定义Hook是React 16.8引入Hooks特性后的重要扩展能力,它允许开发者将组件逻辑提取到可重用的函数中。不同于传统的HOC(高阶组件)或Render Props模式,自定义Hook提供了一种更自然、更符合函数式编程思维的逻辑复用方式。

核心价值与意义

  1. 逻辑复用性:将通用业务逻辑封装成Hook,实现跨组件、跨项目的代码复用
  2. 关注点分离:将复杂组件的状态逻辑与UI渲染分离,提升代码可读性
  3. 组合性:多个自定义Hook可以组合使用,构建复杂功能
  4. 测试友好:纯函数特性使得逻辑测试更加简单
  5. 类型安全:配合TypeScript,提供完整的类型推导

代码实例解析

1. 数据请求Hook封装

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

// 基础数据请求Hook
export function useFetch<T>(url: string, options?: RequestInit) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(url, options);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

// 增强版 - 带缓存和依赖管理的数据请求Hook
export function useQuery<T>(
  key: string | string[],
  fetcher: () => Promise<T>,
  options?: {
    enabled?: boolean;
    staleTime?: number;
    cacheTime?: number;
  }
) {
  const [state, setState] = useState<{
    data: T | null;
    loading: boolean;
    error: Error | null;
  }>({
    data: null,
    loading: true,
    error: null,
  });

  const cacheKey = Array.isArray(key) ? key.join('-') : key;

  useEffect(() => {
    if (options?.enabled === false) return;

    const executeQuery = async () => {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }));
        
        // 简单的缓存实现
        const cached = sessionStorage.getItem(cacheKey);
        if (cached) {
          const cachedData = JSON.parse(cached);
          setState(prev => ({ ...prev, data: cachedData, loading: false }));
        }

        const result = await fetcher();
        
        // 缓存结果
        sessionStorage.setItem(cacheKey, JSON.stringify(result));
        
        setState(prev => ({ ...prev, data: result, loading: false }));
      } catch (err) {
        setState(prev => ({ ...prev, error: err as Error, loading: false }));
      }
    };

    executeQuery();
  }, [cacheKey, fetcher, options?.enabled]);

  return state;
}

// 使用示例
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error } = useQuery(
    ['user', userId],
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    { enabled: !!userId }
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  );
}

2. 表单管理Hook

import { useState, useCallback } from 'react';

export function useForm<T extends Record<string, any>>(
  initialValues: T,
  validators?: Partial<Record<keyof T, (value: any) => string | undefined>>
) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const setValue = useCallback((name: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // 实时验证
    if (validators?.[name]) {
      const error = validators[name]!(value);
      setErrors(prev => ({
        ...prev,
        [name]: error
      }));
    }
  }, [validators]);

  const handleChange = useCallback((
    name: keyof T, 
    value?: any
  ) => {
    if (arguments.length === 1) {
      // 处理事件对象
      return (e: React.ChangeEvent<HTMLInputElement>) => {
        const { value, type, checked } = e.target;
        setValue(name, type === 'checkbox' ? checked : value);
      };
    }
    
    setValue(name, value);
  }, [setValue]);

  const handleBlur = useCallback((name: keyof T) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  const validate = useCallback(() => {
    const newErrors: Partial<Record<keyof T, string>> = {};
    
    if (validators) {
      Object.keys(validators).forEach(key => {
        const validator = validators[key as keyof T];
        if (validator) {
          const error = validator(values[key as keyof T]);
          if (error) {
            newErrors[key as keyof T] = error;
          }
        }
      });
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [values, validators]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    setValue,
    validate,
    reset,
    isValid: Object.keys(errors).length === 0
  };
}

// 使用示例
function LoginForm() {
  const { values, errors, touched, handleChange, handleBlur, validate } = useForm(
    { email: '', password: '' },
    {
      email: (value) => !value ? 'Email is required' : !/\S+@\S+\.\S+/.test(value) ? 'Invalid email' : undefined,
      password: (value) => !value ? 'Password is required' : value.length < 6 ? 'Password too short' : undefined
    }
  );

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (validate()) {
      console.log('Form submitted:', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange('email')}
          onBlur={() => handleBlur('email')}
          placeholder="Email"
        />
        {touched.email && errors.email && <span>{errors.email}</span>}
      </div>
      
      <div>
        <input
          type="password"
          name="password"
          value={values.password}
          onChange={handleChange('password')}
          onBlur={() => handleBlur('password')}
          placeholder="Password"
        />
        {touched.password && errors.password && <span>{errors.password}</span>}
      </div>
      
      <button type="submit">Login</button>
    </form>
  );
}

3. 拖拽功能Hook

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

interface DragOptions {
  onDragStart?: (event: React.DragEvent) => void;
  onDrag?: (event: React.DragEvent, delta: { x: number; y: number }) => void;
  onDragEnd?: (event: React.DragEvent) => void;
}

export function useDrag(options: DragOptions = {}) {
  const [isDragging, setIsDragging] = useState(false);
  const dragStartPos = useRef({ x: 0, y: 0 });
  const elementRef = useRef<HTMLElement>(null);

  const handleDragStart = useCallback((e: React.DragEvent) => {
    setIsDragging(true);
    dragStartPos.current = { x: e.clientX, y: e.clientY };
    
    // 设置拖拽图像
    if (e.dataTransfer) {
      e.dataTransfer.effectAllowed = 'move';
      // 创建透明图像作为拖拽反馈
      const dragImage = document.createElement('div');
      dragImage.style.opacity = '0';
      document.body.appendChild(dragImage);
      e.dataTransfer.setDragImage(dragImage, 0, 0);
      setTimeout(() => document.body.removeChild(dragImage), 0);
    }
    
    options.onDragStart?.(e);
  }, [options]);

  const handleDrag = useCallback((e: React.DragEvent) => {
    if (!isDragging) return;
    
    const delta = {
      x: e.clientX - dragStartPos.current.x,
      y: e.clientY - dragStartPos.current.y
    };
    
    options.onDrag?.(e, delta);
  }, [isDragging, options]);

  const handleDragEnd = useCallback((e: React.DragEvent) => {
    setIsDragging(false);
    options.onDragEnd?.(e);
  }, [options]);

  // 绑定事件到元素
  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const handleMouseDown = (e: MouseEvent) => {
      const dragEvent = e as unknown as React.DragEvent;
      handleDragStart(dragEvent);
    };

    element.addEventListener('mousedown', handleMouseDown);
    return () => element.removeEventListener('mousedown', handleMouseDown);
  }, [handleDragStart]);

  return {
    isDragging,
    elementRef,
    dragHandlers: {
      onDragStart: handleDragStart,
      onDrag: handleDrag,
      onDragEnd: handleDragEnd,
    }
  };
}

// 高级拖拽Hook - 支持边界限制和网格对齐
export function useAdvancedDrag(
  bounds?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
  gridSize: number = 1
) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const isDraggingRef = useRef(false);

  const { elementRef, dragHandlers } = useDrag({
    onDragStart: () => {
      isDraggingRef.current = true;
    },
    onDrag: (e, delta) => {
      if (!isDraggingRef.current) return;

      let newX = Math.round(delta.x / gridSize) * gridSize;
      let newY = Math.round(delta.y / gridSize) * gridSize;

      // 边界检查
      if (bounds) {
        if (bounds.minX !== undefined) newX = Math.max(bounds.minX, newX);
        if (bounds.maxX !== undefined) newX = Math.min(bounds.maxX, newX);
        if (bounds.minY !== undefined) newY = Math.max(bounds.minY, newY);
        if (bounds.maxY !== undefined) newY = Math.min(bounds.maxY, newY);
      }

      setPosition({ x: newX, y: newY });
    },
    onDragEnd: () => {
      isDraggingRef.current = false;
    }
  });

  return {
    position,
    elementRef,
    dragHandlers,
    setPosition
  };
}

// 使用示例 - 可拖拽组件
function DraggableItem() {
  const { position, elementRef, dragHandlers } = useAdvancedDrag(
    { minX: 0, maxX: 500, minY: 0, maxY: 500 },
    10 // 10px网格对齐
  );

  return (
    <div
      ref={elementRef as React.RefObject<HTMLDivElement>}
      {...dragHandlers}
      style={{
        position: 'absolute',
        left: position.x,
        top: position.y,
        width: 100,
        height: 100,
        backgroundColor: 'lightblue',
        cursor: 'move',
        userSelect: 'none',
        border: '1px solid #ccc'
      }}
    >
      拖拽我
    </div>
  );
}

// 使用示例 - 拖拽列表
function DraggableList() {
  const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
  
  const handleDragStart = (index: number) => (e: React.DragEvent) => {
    e.dataTransfer.setData('text/plain', index.toString());
  };

  const handleDrop = (targetIndex: number) => (e: React.DragEvent) => {
    e.preventDefault();
    const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));
    
    if (sourceIndex !== targetIndex) {
      const newItems = [...items];
      const [movedItem] = newItems.splice(sourceIndex, 1);
      newItems.splice(targetIndex, 0, movedItem);
      setItems(newItems);
    }
  };

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
  };

  return (
    <div>
      {items.map((item, index) => (
        <div
          key={index}
          draggable
          onDragStart={handleDragStart(index)}
          onDrop={handleDrop(index)}
          onDragOver={handleDragOver}
          style={{
            padding: '10px',
            margin: '5px',
            backgroundColor: 'white',
            border: '1px solid #ddd',
            cursor: 'move'
          }}
        >
          {item}
        </div>
      ))}
    </div>
  );
}

封装艺术与最佳实践

设计原则

  1. 单一职责:每个Hook专注于解决一个特定问题
  2. 组合优先:通过组合简单Hook构建复杂功能
  3. 依赖明确:清晰定义输入输出,避免隐式依赖
  4. 错误边界:提供适当的错误处理和恢复机制

进阶模式

// Hook工厂模式 - 创建可配置的Hook
export function createFetchHook<T>(baseOptions: RequestInit = {}) {
  return function useCustomFetch(url: string, options?: RequestInit) {
    const mergedOptions = { ...baseOptions, ...options };
    return useFetch<T>(url, mergedOptions);
  };
}

// 创建带有认证头的专用Fetch Hook
export const useAuthFetch = createFetchHook({
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('token')}`,
    'Content-Type': 'application/json'
  }
});

// Hook组合示例
export function useUserDashboard(userId: string) {
  const user = useQuery(['user', userId], () => fetchUser(userId));
  const posts = useQuery(['posts', userId], () => fetchUserPosts(userId));
  const stats = useQuery(['stats', userId], () => fetchUserStats(userId));

  const isLoading = user.loading || posts.loading || stats.loading;
  const error = user.error || posts.error || stats.error;

  return {
    user: user.data,
    posts: posts.data,
    stats: stats.data,
    isLoading,
    error,
    refetch: () => {
      user.refetch?.();
      posts.refetch?.();
      stats.refetch?.();
    }
  };
}

总结

自定义Hook是现代React开发的"炼金术",它将复杂的业务逻辑转化为可重用、可组合、可测试的代码单元。从简单的数据请求到复杂的拖拽交互,合理的Hook封装能够显著提升代码质量、开发效率和团队协作能力。掌握这门艺术,意味着掌握了构建可维护、可扩展React应用的关键技能。

通过本文的实例可以看到,优秀的自定义Hook设计不仅解决了具体的技术问题,更重要的是建立了一套可扩展的模式和架构思想,这正是前端工程化向更高层次迈进的重要标志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小灰灰学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值