Hooks 进阶:自定义 Hook 的设计与实践

引言

React Hooks 已成为现代 React 开发的核心范式,而自定义 Hook 则为我们提供了强大的代码复用机制。

自定义 Hook 的基础原理

自定义 Hook 本质上是一种函数复用机制,它允许我们将组件逻辑提取到可重用的函数中。与传统的高阶组件(HOC)和 render props 模式相比,Hook 提供了更直接的状态共享方式,不会引入额外的组件嵌套。

自定义 Hook 的核心规则

  1. 命名必须以 use 开头:这不仅是约定,也使 React 能够识别 Hook 函数
  2. 可以调用其他 Hook:自定义 Hook 内部可以调用 React 内置 Hook 或其他自定义 Hook
  3. 状态是隔离的:不同组件调用同一个 Hook 不会共享状态
// 基础自定义 Hook 示例
function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + step);
  }, [step]);
  
  const decrement = useCallback(() => {
    setCount(prevCount => prevCount - step);
  }, [step]);
  
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return { count, increment, decrement, reset };
}

// 使用示例
function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(10, 2);
  
  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

自定义 Hook 设计模式

1. 资源管理型 Hook

这类 Hook 负责管理外部资源的生命周期,如网络请求、事件监听等。

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const optionsRef = useRef(options);
  
  useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();
    const signal = controller.signal;
    
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url, {
          ...optionsRef.current,
          signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const result = await response.json();
        if (isMounted) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (isMounted && err.name !== 'AbortError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [url]);
  
  return { data, loading, error };
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

2. 状态逻辑型 Hook

封装复杂状态逻辑,提供简洁的状态管理接口。

function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleChange = useCallback((e) => {
    const { name, value } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
  }, []);
  
  const handleBlur = useCallback((e) => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
  }, []);
  
  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsSubmitting(false);
  }, [initialValues]);
  
  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    setValues,
    setErrors,
    setIsSubmitting,
    reset
  };
}

// 使用示例
function LoginForm() {
  const { 
    values, 
    errors, 
    touched, 
    isSubmitting, 
    handleChange, 
    handleBlur, 
    setErrors, 
    setIsSubmitting 
  } = useForm({ email: '', password: '' });
  
  const validate = () => {
    const newErrors = {};
    if (!values.email) newErrors.email = '邮箱不能为空';
    if (!values.password) newErrors.password = '密码不能为空';
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!validate()) return;
    
    setIsSubmitting(true);
    try {
      // 登录逻辑
      await loginUser(values);
      alert('登录成功');
    } catch (err) {
      setErrors({ form: err.message });
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">邮箱</label>
        <input
          id="email"
          name="email"
          type="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && <div className="error">{errors.email}</div>}
      </div>
      
      <div>
        <label htmlFor="password">密码</label>
        <input
          id="password"
          name="password"
          type="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && <div className="error">{errors.password}</div>}
      </div>
      
      {errors.form && <div className="error">{errors.form}</div>}
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

3. 行为型 Hook

封装特定用户交互行为的逻辑,如拖拽、虚拟滚动等。

function useDrag(ref, options = {}) {
  const {
    onDragStart,
    onDrag,
    onDragEnd,
    disabled = false
  } = options;
  
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const startPosRef = useRef({ x: 0, y: 0 });
  const currentPosRef = useRef({ x: 0, y: 0 });
  
  useEffect(() => {
    if (!ref.current || disabled) return;
    
    const element = ref.current;
    
    const handleMouseDown = (e) => {
      // 避免与点击事件冲突
      if (e.button !== 0) return;
      
      setIsDragging(true);
      startPosRef.current = {
        x: e.clientX - currentPosRef.current.x,
        y: e.clientY - currentPosRef.current.y
      };
      
      if (onDragStart) {
        onDragStart({
          x: currentPosRef.current.x,
          y: currentPosRef.current.y
        });
      }
      
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      e.preventDefault();
    };
    
    const handleMouseMove = (e) => {
      if (!isDragging) return;
      
      const newPos = {
        x: e.clientX - startPosRef.current.x,
        y: e.clientY - startPosRef.current.y
      };
      
      currentPosRef.current = newPos;
      setPosition(newPos);
      
      if (onDrag) {
        onDrag(newPos);
      }
    };
    
    const handleMouseUp = () => {
      setIsDragging(false);
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
      
      if (onDragEnd) {
        onDragEnd({
          x: currentPosRef.current.x,
          y: currentPosRef.current.y
        });
      }
    };
    
    element.addEventListener('mousedown', handleMouseDown);
    
    return () => {
      element.removeEventListener('mousedown', handleMouseDown);
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [ref, disabled, isDragging, onDragStart, onDrag, onDragEnd]);
  
  return { isDragging, position, setPosition };
}

// 使用示例
function DraggableBox() {
  const boxRef = useRef(null);
  const { isDragging, position } = useDrag(boxRef, {
    onDragStart: (pos) => console.log('开始拖动', pos),
    onDragEnd: (pos) => console.log('结束拖动', pos)
  });
  
  return (
    <div
      ref={boxRef}
      style={{
        position: 'absolute',
        left: `${position.x}px`,
        top: `${position.y}px`,
        width: '100px',
        height: '100px',
        background: isDragging ? '#5c7cfa' : '#339af0',
        cursor: 'grab',
        userSelect: 'none',
        boxShadow: isDragging ? '0 8px 16px rgba(0,0,0,0.2)' : '0 2px 4px rgba(0,0,0,0.1)',
        transition: isDragging ? 'none' : 'box-shadow 0.3s, background 0.3s'
      }}
    >
      拖拽我
    </div>
  );
}

高级技巧与优化

1. 依赖收集与性能优化

自定义 Hook 的性能优化主要关注两个方面:减少不必要的重渲染和优化内部逻辑执行效率。

function useSearch(initialQuery = '') {
  const [query, setQuery] = useState(initialQuery);
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // 使用 useRef 保存最新值,避免 useCallback 和 useEffect 依赖过多
  const stateRef = useRef({ query });
  stateRef.current.query = query;
  
  // 使用 useCallback 缓存函数引用
  const search = useCallback(debounce(async () => {
    const currentQuery = stateRef.current.query;
    if (!currentQuery.trim()) {
      setResults([]);
      return;
    }
    
    setLoading(true);
    try {
      const response = await fetch(`https://api.example.com/search?q=${encodeURIComponent(currentQuery)}`);
      const data = await response.json();
      setResults(data);
    } catch (error) {
      console.error('搜索出错:', error);
      setResults([]);
    } finally {
      setLoading(false);
    }
  }, 300), []);
  
  // 查询变化时触发搜索
  useEffect(() => {
    search();
    
    // 返回清理函数,取消正在进行的请求
    return () => {
      search.cancel();
    };
  }, [query, search]);
  
  return {
    query,
    setQuery,
    results,
    loading
  };
}

// 节流/防抖辅助函数
function debounce(fn, delay) {
  let timer = null;
  
  const debounced = function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
  
  debounced.cancel = function() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };
  
  return debounced;
}

2. 组合 Hooks 实现复杂功能

通过组合多个基础 Hook 实现更复杂的功能,遵循单一职责原则。

// 基础 Hook: 管理分页状态
function usePagination(initialPage = 1, initialPageSize = 10) {
  const [page, setPage] = useState(initialPage);
  const [pageSize, setPageSize] = useState(initialPageSize);
  
  const reset = useCallback(() => {
    setPage(initialPage);
    setPageSize(initialPageSize);
  }, [initialPage, initialPageSize]);
  
  return {
    page,
    pageSize,
    setPage,
    setPageSize,
    reset
  };
}

// 基础 Hook: 管理排序状态
function useSorting(initialSortField = '', initialSortDirection = 'asc') {
  const [sortField, setSortField] = useState(initialSortField);
  const [sortDirection, setSortDirection] = useState(initialSortDirection);
  
  const toggleSort = useCallback((field) => {
    if (field === sortField) {
      setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
    } else {
      setSortField(field);
      setSortDirection('asc');
    }
  }, [sortField]);
  
  return {
    sortField,
    sortDirection,
    toggleSort
  };
}

// 组合 Hook: 实现数据表格功能
function useDataTable(fetchFn, initialFilters = {}) {
  const [filters, setFilters] = useState(initialFilters);
  const [data, setData] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // 组合分页 Hook
  const pagination = usePagination();
  
  // 组合排序 Hook
  const sorting = useSorting();
  
  // 加载数据的函数
  const loadData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const params = {
        page: pagination.page,
        pageSize: pagination.pageSize,
        sortField: sorting.sortField,
        sortDirection: sorting.sortDirection,
        ...filters
      };
      
      const result = await fetchFn(params);
      setData(result.data);
      setTotal(result.total);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [
    fetchFn,
    pagination.page, 
    pagination.pageSize, 
    sorting.sortField, 
    sorting.sortDirection, 
    filters
  ]);
  
  // 过滤条件、分页或排序变化时重新加载数据
  useEffect(() => {
    loadData();
  }, [loadData]);
  
  // 更新过滤条件的函数
  const updateFilters = useCallback((newFilters) => {
    setFilters(prev => ({
      ...prev,
      ...newFilters
    }));
    // 重置到第一页
    pagination.setPage(1);
  }, [pagination]);
  
  // 重置所有状态
  const reset = useCallback(() => {
    pagination.reset();
    setFilters(initialFilters);
  }, [pagination, initialFilters]);
  
  return {
    // 数据状态
    data,
    total,
    loading,
    error,
    // 分页相关
    page: pagination.page,
    pageSize: pagination.pageSize,
    setPage: pagination.setPage,
    setPageSize: pagination.setPageSize,
    // 排序相关
    sortField: sorting.sortField,
    sortDirection: sorting.sortDirection,
    toggleSort: sorting.toggleSort,
    // 过滤相关
    filters,
    updateFilters,
    // 操作方法
    reload: loadData,
    reset
  };
}

// 使用示例
function UsersTable() {
  const fetchUsers = async (params) => {
    const queryString = new URLSearchParams(params).toString();
    const response = await fetch(`https://api.example.com/users?${queryString}`);
    return await response.json();
  };
  
  const {
    data: users,
    total,
    loading,
    page,
    pageSize,
    setPage,
    setPageSize,
    sortField,
    sortDirection,
    toggleSort,
    filters,
    updateFilters
  } = useDataTable(fetchUsers, { status: 'active' });
  
  return (
    <div>
      <div className="filters">
        <input
          placeholder="搜索用户"
          value={filters.keyword || ''}
          onChange={e => updateFilters({ keyword: e.target.value })}
        />
        <select
          value={filters.status}
          onChange={e => updateFilters({ status: e.target.value })}
        >
          <option value="active">活跃</option>
          <option value="inactive">非活跃</option>
          <option value="all">全部</option>
        </select>
      </div>
      
      {loading ? (
        <div>加载中...</div>
      ) : (
        <table>
          <thead>
            <tr>
              <th onClick={() => toggleSort('name')}>
                姓名 {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
              </th>
              <th onClick={() => toggleSort('email')}>
                邮箱 {sortField === 'email' && (sortDirection === 'asc' ? '↑' : '↓')}
              </th>
              <th onClick={() => toggleSort('lastLogin')}>
                最近登录 {sortField === 'lastLogin' && (sortDirection === 'asc' ? '↑' : '↓')}
              </th>
            </tr>
          </thead>
          <tbody>
            {users.map(user => (
              <tr key={user.id}>
                <td>{user.name}</td>
                <td>{user.email}</td>
                <td>{new Date(user.lastLogin).toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      
      <div className="pagination">
        <button 
          disabled={page === 1} 
          onClick={() => setPage(page - 1)}
        >
          上一页
        </button>
        <span>第 {page} 页,共 {Math.ceil(total / pageSize)} 页</span>
        <button 
          disabled={page >= Math.ceil(total / pageSize)}
          onClick={() => setPage(page + 1)}
        >
          下一页
        </button>
        <select
          value={pageSize}
          onChange={e => setPageSize(Number(e.target.value))}
        >
          <option value={10}>10条/页</option>
          <option value={20}>20条/页</option>
          <option value={50}>50条/页</option>
        </select>
      </div>
    </div>
  );
}

3. 利用 Context 优化 Hook 共享状态

当多个组件需要共享同一个 Hook 的状态时,可以结合 Context API 实现。

// 创建一个主题上下文
const ThemeContext = createContext(null);

// 主题 Provider 组件
function ThemeProvider({ children, initialTheme = 'light' }) {
  const [theme, setTheme] = useState(initialTheme);
  
  // 在 localStorage 中保存主题偏好
  useEffect(() => {
    localStorage.setItem('theme', theme);
  }, [theme]);
  
  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);
  
  // 创建主题值对象
  const themeValue = useMemo(() => ({
    theme,
    setTheme,
    toggleTheme,
    isDark: theme === 'dark'
  }), [theme, toggleTheme]);
  
  return (
    <ThemeContext.Provider value={themeValue}>
      {children}
    </ThemeContext.Provider>
  );
}

// 自定义 Hook 用于访问主题上下文
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error('useTheme 必须在 ThemeProvider 内部使用');
  }
  return context;
}

// 使用示例
function App() {
  return (
    <ThemeProvider initialTheme="light">
      <MainLayout />
    </ThemeProvider>
  );
}

function MainLayout() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <div className={`app ${theme}`}>
      <header>
        <h1>我的应用</h1>
        <button onClick={toggleTheme}>
          切换到{theme === 'light' ? '暗色' : '亮色'}主题
        </button>
      </header>
      <main>
        <Content />
      </main>
    </div>
  );
}

function Content() {
  const { isDark } = useTheme();
  
  return (
    <section className="content">
      <h2>内容区域</h2>
      <p>当前使用的是{isDark ? '暗色' : '亮色'}主题</p>
    </section>
  );
}

自定义 Hook 与现代前端架构

1. 与状态管理的整合

自定义 Hook 可以与 Redux、Zustand 等状态管理库无缝集成,提供更集中的状态管理方案。

// 集成 Redux 的自定义 Hook
function useReduxActions(slice) {
  const dispatch = useDispatch();
  const state = useSelector(state => state[slice]);
  
  // 使用 useMemo 缓存创建的 actions 对象
  const actions = useMemo(() => {
    // 示例:为一个用户模块创建actions
    if (slice === 'users') {
      return {
        fetchUsers: (params) => {
          dispatch({ type: 'users/fetchUsersStart', payload: params });
          return fetch(`/api/users?${new URLSearchParams(params)}`)
            .then(res => res.json())
            .then(data => {
              dispatch({ type: 'users/fetchUsersSuccess', payload: data });
              return data;
            })
            .catch(error => {
              dispatch({ type: 'users/fetchUsersFailure', payload: error.message });
              throw error;
            });
        },
        createUser: (userData) => {
          dispatch({ type: 'users/createUserStart', payload: userData });
          return fetch('/api/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(userData)
          })
            .then(res => res.json())
            .then(data => {
              dispatch({ type: 'users/createUserSuccess', payload: data });
              return data;
            })
            .catch(error => {
              dispatch({ type: 'users/createUserFailure', payload: error.message });
              throw error;
            });
        }
      };
    }
    
    return {};
  }, [dispatch, slice]);
  
  return { state, ...actions };
}

// 使用示例
function UserList() {
  const { state: usersState, fetchUsers } = useReduxActions('users');
  const { data, loading, error } = usersState;
  
  useEffect(() => {
    fetchUsers({ page: 1, limit: 10 });
  }, [fetchUsers]);
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. 与组件库的协同设计

自定义 Hook 可以成为组件库的强大辅助工具,为复杂组件提供逻辑层抽象。

// 自定义 Hook 和组件协同
function useMenuControl(initialOpenKeys = []) {
  const [openKeys, setOpenKeys] = useState(initialOpenKeys);
  const [selectedKey, setSelectedKey] = useState(null);
  
  const onOpenChange = useCallback((key) => {
    setOpenKeys(prev => {
      const keyIndex = prev.indexOf(key);
      if (keyIndex >= 0) {
        // 已打开,则关闭
        const newKeys = [...prev];
        newKeys.splice(keyIndex, 1);
        return newKeys;
      } else {
        // 未打开,则添加
        return [...prev, key];
      }
    });
  }, []);
  
  const isOpen = useCallback((key) => {
    return openKeys.includes(key);
  }, [openKeys]);
  
  return {
    openKeys,
    selectedKey,
    setSelectedKey,
    onOpenChange,
    isOpen
  };
}

// 菜单组件
function Menu({ items, defaultOpenKeys = [] }) {
  const {
    openKeys,
    selectedKey,
    setSelectedKey,
    onOpenChange,
    isOpen
  } = useMenuControl(defaultOpenKeys);
  
  return (
    <nav className="menu">
      {items.map(item => {
        if (item.children) {
          return (
            <div key={item.key} className="submenu">
              <div
                className="submenu-title"
                onClick={() => onOpenChange(item.key)}
              >
                {item.icon && <span className="icon">{item.icon}</span>}
                <span>{item.label}</span>
                <span className={`arrow ${isOpen(item.key) ? 'open' : ''}`}>▾</span>
              </div>
              
              {isOpen(item.key) && (
                <div className="submenu-items">
                  {item.children.map(child => (
                    <div
                      key={child.key}
                      className={`menu-item ${selectedKey === child.key ? 'active' : ''}`}
                      onClick={() => setSelectedKey(child.key)}
                    >
                      {child.icon && <span className="icon">{child.icon}</span>}
                      <span>{child.label}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
          );
        }
        
        return (
          <div
            key={item.key}
            className={`menu-item ${selectedKey === item.key ? 'active' : ''}`}
            onClick={() => setSelectedKey(item.key)}
          >
            {item.icon && <span className="icon">{item.icon}</span>}
            <span>{item.label}</span>
          </div>
        );
      })}
    </nav>
  );
}

// 使用示例
function SidebarNavigation() {
  const menuItems = [
    {
      key: 'dashboard',
      label: '仪表盘',
      icon: '📊'
    },
    {
      key: 'users',
      label: '用户管理',
      icon: '👥',
      children: [
        {
          key: 'user-list',
          label: '用户列表'
        },
        {
          key: 'user-groups',
          label: '用户组'
        }
      ]
    },
    {
      key: 'settings',
      label: '系统设置',
      icon: '⚙️',
      children: [
        {
          key: 'profile',
          label: '个人资料'
        },
        {
          key: 'security',
          label: '安全设置'
        }
      ]
    }
  ];
  
  return (
    <div className="sidebar">
      <div className="logo">应用名称</div>
      <Menu items={menuItems} defaultOpenKeys={['users']} />
    </div>
  );
}

自定义 Hook 测试最佳实践

测试自定义 Hook 是确保其可靠性和可维护性的关键环节。以下是针对自定义 Hook 的测试策略:

// 示例:使用 @testing-library/react-hooks 测试自定义 Hook

// useCounter.js
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + step);
  }, [step]);
  
  const decrement = useCallback(() => {
    setCount(prevCount => prevCount - step);
  }, [step]);
  
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return { count, increment, decrement, reset };
}

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

describe('useCounter', () => {
  test('应该使用默认初始值', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });
  
  test('应该使用提供的初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });
  
  test('应该递增计数', () => {
    const { result } = renderHook(() => useCounter(0, 2));
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(2);
  });
  
  test('应该递减计数', () => {
    const { result } = renderHook(() => useCounter(10, 5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(5);
  });
  
  test('应该重置计数', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });
  
  test('当步长变化时应该更新递增/递减行为', () => {
    const { result, rerender } = renderHook(
      ({ initialValue, step }) => useCounter(initialValue, step),
      { initialProps: { initialValue: 0, step: 1 } }
    );
    
    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(1);
    
    // 更新 step 参数
    rerender({ initialValue: 0, step: 3 });
    
    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(4); // 1 + 3
  });
});

实际项目应用

在实际项目中应用自定义 Hook 时,应采取以下建议:

  1. 职责单一:每个 Hook 应专注于单一功能,避免过度复杂
  2. 明确的命名:使用描述性的名称清晰表达 Hook 的用途
  3. 文档完善:为每个 Hook 编写详细文档,包括参数、返回值和使用示例
  4. 版本控制:随着 API 的演进,保持版本兼容性并提供迁移路径
  5. 优先考虑性能:使用 useCallback、useMemo 优化 Hook 内部逻辑

结语

自定义 Hook 是 React 应用开发中强大的抽象工具,能够显著提升代码复用性和可维护性。

未来,随着 React 生态的不断发展,自定义 Hook 的设计模式也将继续演进。保持学习新的模式和技术,才能帮助我们在前端开发领域保持竞争力。

参考资源

官方文档

技术博客和文章

测试资源

社区讨论

高级模式和实践


如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值