mcpField
R
react-hooks-composition

react-hooks-composition

github.com/bobmatnyc开发78190 次安装15 周前更新
代码仓库

关于 react-hooks-composition

react-hooks-composition 是面向 React 和 TypeScript 开发者的自定义 Hooks 组合模式指南,涵盖 SWR 数据获取、搜索防抖、上下文优化和状态机。

主要功能

  1. 01SWR 条件式数据获取
  2. 02防抖搜索与双重加载状态
  3. 03记忆化 Context Provider
  4. 04类型安全的状态机
  5. 05纯辅助函数便于测试

适用场景

  1. 01按用户输入条件请求数据
  2. 02减少搜索输入时的 API 请求
  3. 03避免 Context 消费者不必要重渲染
  4. 04管理异步操作的加载与错误状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill react-hooks-composition

安装后在 Claude Code 中输入/react-hooks-composition确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.json
references/
performance-optimization.md
swr-patterns.md
testing-patterns.md

详细介绍

React Hooks 组合模式

概述

用于组合 React Hooks 的高级模式,可创建易于维护、高性能且类型安全的自定义 Hook。涵盖 SWR 集成、防抖搜索、记忆化上下文和状态机模式。

关键概念:

  • 使用 null 键进行条件 SWR 获取
  • 带双重加载指示器的防抖状态
  • 使用记忆化上下文提供程序避免重新渲染
  • 用于实现可预测 UI 状态的状态机模式
  • 便于测试的纯辅助函数

模式 1:使用条件获取组合 SWR Hook

模式

将 SWR 与条件逻辑、数据转换和纯辅助函数组合使用。

import useSWR from 'swr';
import { useMemo } from 'react';

// Type definitions
interface MapboxSuggestion {
  name: string;
  mapbox_id: string;
  context?: {
    country?: {
      country_code: string;
    };
  };
}

interface MapboxSuggestResponse {
  suggestions: MapboxSuggestion[];
}

interface LocationSuggestion {
  id: string;
  displayName: string;
  region: string;
}

// Custom hook with conditional fetching
export function useMapboxLocationSuggestions(
  inputValue: string | null | undefined
) {
  const sessionId = useSessionId();

  // Conditional SWR key - null disables fetching
  const { data, error, isLoading } = useSWR<MapboxSuggestResponse>(
    // Key is null (no fetch) unless all conditions met
    sessionId &&
    process.env.NEXT_PUBLIC_MAPBOX_API_KEY &&
    isValidSearchQuery(inputValue)
      ? `https://api.mapbox.com/search/searchbox/v1/suggest?q=${encodeURIComponent(inputValue!)}&session_token=${sessionId}&access_token=${process.env.NEXT_PUBLIC_MAPBOX_API_KEY}`
      : null
  );

  // Transform data with useMemo for performance
  const mappedData = useMemo(() => {
    if (!data) return undefined;

    return data.suggestions
      .filter(isUsState)
      .map(formatMapboxLocation);
  }, [data]);

  return {
    data: mappedData,
    error,
    isLoading
  };
}

// Pure helper functions (outside component/hook)
const isValidSearchQuery = (
  value: string | null | undefined
): value is string => {
  return typeof value === 'string' && value.trim().length >= 2;
};

const isUsState = (suggestion: MapboxSuggestion): boolean => {
  return suggestion.context?.country?.country_code === 'us';
};

const formatMapboxLocation = (
  suggestion: MapboxSuggestion
): LocationSuggestion => ({
  id: suggestion.mapbox_id,
  displayName: suggestion.name,
  region: 'US',
});
原理

条件获取:

  • 当键为 null 时,SWR 不会获取数据
  • 构造 URL 前检查所有条件
  • 类型守卫 isValidSearchQuery 可确保类型安全
  • 避免在输入为空时进行不必要的 API 调用

数据转换:

  • useMemo 可避免每次渲染时都重新计算
  • 依赖数组 [data] 仅在数据变化时触发重新计算
  • 过滤和映射操作都是纯操作,便于测试

可测试性:

  • 可以独立测试纯函数
  • 辅助函数无需 React 上下文
  • 类型守卫可提供运行时验证
常见场景

适用于以下场景:

  • “根据用户输入获取数据”
  • “使用 SWR 进行条件 API 调用”
  • “转换 API 响应数据”
  • “避免在搜索内容为空时获取数据”
  • “类型安全的数据获取 Hook”

模式 2:带双重加载状态的防抖搜索

模式

将防抖输入与 SWR 数据获取结合使用,同时跟踪防抖和网络加载状态。

import { useState } from 'react';
import useSWR from 'swr';

// Debounce hook
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Search component with dual loading states
export function SearchComponent() {
  const [searchInput, setSearchInput] = useState('');
  const debouncedSearchInput = useDebounce(searchInput, 300);

  // Track if we're still debouncing (user is typing)
  const isDebouncing = searchInput !== debouncedSearchInput;

  // Fetch with debounced value
  const { data = [], isLoading } = useLocationSuggestions(debouncedSearchInput);

  // Combined loading state: debouncing OR fetching
  const suggestionsLoading = isDebouncing || isLoading;

  return (
    <SearchInput
      value={searchInput}
      onChange={setSearchInput}
      loading={suggestionsLoading}
      suggestions={data}
      placeholder="Search locations..."
    />
  );
}
原理

双重加载状态:

  • isDebouncing:表示用户仍在输入
  • isLoading:表示网络请求正在进行
  • 组合状态可提供流畅的 UX 反馈

性能:

  • 只有在用户停止操作 300ms 后才会发起 API 调用
  • 减少输入期间不必要的请求
  • SWR 会缓存结果,以便即时显示

用户体验:

  • 用户开始输入时立即显示加载指示器
  • 避免在防抖间隔期间结果闪烁
  • 在输入和获取数据阶段都提供清晰的反馈
常见场景

适用于以下场景:

  • “带加载状态的防抖搜索”
  • “避免输入时发起过多 API 调用”
  • “防抖期间显示加载指示器”
  • “流畅的搜索体验”
  • “将防抖与数据获取结合使用”

模式 3:记忆化上下文提供程序

模式

使用记忆化值创建上下文提供程序,避免消费者不必要地重新渲染。

import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react';

// Context value type
interface LocationContextValue {
  location: UserLocation | null;
  requestPreciseLocation: () => Promise<UserLocation | null>;
}

interface UserLocation {
  latitude: number;
  longitude: number;
  accuracy: 'coarse' | 'precise';
}

// Create context with undefined default
const UserLocationContext = createContext<LocationContextValue | undefined>(
  undefined
);

// Provider component
export const UserLocationProvider = ({
  location: initialLocation = null,
  children
}: {
  location?: UserLocation | null;
  children: ReactNode;
}) => {
  const [preciseLocation, setPreciseLocation] = useState<UserLocation | null>(null);

  // Stable callback reference with useCallback
  const requestPreciseLocation = useCallback(async () => {
    try {
      const coords = await getUserCoordinates();
      const location = coords
        ? {
            latitude: coords.latitude,
            longitude: coords.longitude,
            accuracy: 'precise' as const
          }
        : null;

      setPreciseLocation(location);
      return location;
    } catch (error) {
      console.error('Failed to get precise location:', error);
      return null;
    }
  }, []); // No dependencies - function is stable

  // Memoized context value prevents unnecessary re-renders
  const contextValue = useMemo<LocationContextValue>(() => ({
    location: preciseLocation || initialLocation,
    requestPreciseLocation,
  }), [initialLocation, preciseLocation, requestPreciseLocation]);

  return (
    <UserLocationContext.Provider value={contextValue}>
      {children}
    </UserLocationContext.Provider>
  );
};

// Type-safe hook for consuming context
export const useUserLocation = (): LocationContextValue => {
  const context = useContext(UserLocationContext);

  if (context === undefined) {
    throw new Error('useUserLocation must be used within UserLocationProvider');
  }

  return context;
};

// Helper function (can be in separate file)
async function getUserCoordinates(): Promise<GeolocationCoordinates | null> {
  return new Promise((resolve) => {
    navigator.geolocation.getCurrentPosition(
      (position) => resolve(position.coords),
      () => resolve(null),
      { enableHighAccuracy: true }
    );
  });
}
原理

记忆化可避免重新渲染:

  • 使用 useMemo 记忆化上下文值对象
  • 仅在依赖项变化时重新创建
  • 仅当值实际变化时,消费者才会重新渲染

稳定引用:

  • useCallback 可确保 requestPreciseLocation 引用稳定
  • 不设置依赖项意味着函数永远不会变化
  • 内部状态更新不会重新创建函数

类型安全:

  • 在提供程序外使用上下文 Hook 时会抛出错误
  • TypeScript 会强制确保正确使用
  • 滥用时会提供清晰的错误信息

最佳实践:

  • 上下文值是包含多个属性的对象
  • 提供程序同时管理属性和内部状态
  • 自定义 Hook 可封装上下文消费逻辑
常见场景

适用于以下场景:

  • “上下文导致过多重新渲染”
  • “优化上下文提供程序的性能”
  • “上下文中的稳定回调”
  • “记忆化上下文值”
  • “避免上下文导致不必要的渲染”

模式 4:用于 UI 状态的状态机

模式

使用判别联合和状态机,实现可预测的 UI 状态管理。

import { useState } from 'react';

// Discriminated union for request states
type RequestState = 'idle' | 'pending' | 'success' | 'error';

// State-specific configuration
const stateConfig: Record<RequestState, {
  label: string;
  disabled: boolean;
  variant: 'primary' | 'success' | 'danger';
}> = {
  idle: {
    label: 'Click to start',
    disabled: false,
    variant: 'primary',
  },
  pending: {
    label: 'Loading...',
    disabled: true,
    variant: 'primary',
  },
  success: {
    label: 'Complete!',
    disabled: false,
    variant: 'success',
  },
  error: {
    label: 'Failed - try again',
    disabled: false,
    variant: 'danger',
  },
};

// Component using state machine
export function AsyncButton({
  onClick
}: {
  onClick: () => Promise<void>;
}) {
  const [state, setState] = useState<RequestState>('idle');

  const handleClick = async () => {
    // State transition: idle/error → pending
    setState('pending');

    try {
      await onClick();
      // State transition: pending → success
      setState('success');

      // Auto-reset after 2 seconds
      setTimeout(() => setState('idle'), 2000);
    } catch (error) {
      // State transition: pending → error
      setState('error');
    }
  };

  const config = stateConfig[state];

  return (
    <button
      onClick={handleClick}
      disabled={config.disabled}
      className={`btn btn-${config.variant}`}
    >
      {config.label}
    </button>
  );
}
进阶:携带数据的标记联合

对于带有特定状态数据的复杂状态:

// Tagged union with discriminated state
type AsyncState<T, E = Error> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: E };

function useAsyncOperation<T>(
  operation: () => Promise<T>
): {
  state: AsyncState<T>;
  execute: () => Promise<void>;
  reset: () => void;
} {
  const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });

  const execute = async () => {
    setState({ status: 'loading' });

    try {
      const data = await operation();
      setState({ status: 'success', data });
    } catch (error) {
      setState({
        status: 'error',
        error: error instanceof Error ? error : new Error('Unknown error')
      });
    }
  };

  const reset = () => {
    setState({ status: 'idle' });
  };

  return { state, execute, reset };
}

// Usage in component
export function DataFetchButton() {
  const { state, execute, reset } = useAsyncOperation(async () => {
    const response = await fetch('/api/data');
    return response.json();
  });

  return (
    <div>
      <button onClick={execute} disabled={state.status === 'loading'}>
        Fetch Data
      </button>

      {state.status === 'loading' && <Spinner />}

      {state.status === 'success' && (
        <div>
          <pre>{JSON.stringify(state.data, null, 2)}</pre>
          <button onClick={reset}>Reset</button>
        </div>
      )}

      {state.status === 'error' && (
        <div className="error">
          {state.error.message}
          <button onClick={reset}>Try Again</button>
        </div>
      )}
    </div>
  );
}
这样做的原因

可预测的状态转换:

  • 显式状态可防止出现不可能的状态
  • 状态转换逻辑清晰
  • TypeScript 确保处理所有状态

类型安全:

  • 可辨识联合支持穷尽检查
  • 特定状态的数据具有类型安全性
  • 编译器可检测遗漏的状态处理器

易于维护:

  • 状态配置集中在一处
  • 易于添加新状态
  • 状态与 UI 清晰分离
常见触发场景

在以下情况下使用此模式:

  • “管理异步操作状态”
  • “可预测的 UI 状态机”
  • “加载、成功、错误状态”
  • “防止出现不可能的状态”
  • “类型安全的状态管理”

高级组合:组合模式

完整的搜索组件

组合防抖搜索、SWR 数据获取和状态机:

import { useState, useMemo } from 'react';
import useSWR from 'swr';

type SearchState = 'idle' | 'debouncing' | 'fetching' | 'success' | 'error';

export function AdvancedSearch() {
  const [searchInput, setSearchInput] = useState('');
  const debouncedInput = useDebounce(searchInput, 300);

  // Determine current state
  const isDebouncing = searchInput !== debouncedInput;

  // Conditional SWR fetching
  const { data, error, isLoading } = useSWR(
    debouncedInput.length >= 2
      ? `/api/search?q=${encodeURIComponent(debouncedInput)}`
      : null
  );

  // Compute search state
  const searchState = useMemo<SearchState>(() => {
    if (error) return 'error';
    if (isDebouncing) return 'debouncing';
    if (isLoading) return 'fetching';
    if (data) return 'success';
    return 'idle';
  }, [isDebouncing, isLoading, data, error]);

  // State-specific UI
  const showSpinner = searchState === 'debouncing' || searchState === 'fetching';
  const showResults = searchState === 'success';
  const showError = searchState === 'error';

  return (
    <div>
      <input
        type="text"
        value={searchInput}
        onChange={(e) => setSearchInput(e.target.value)}
        placeholder="Search..."
      />

      {showSpinner && <Spinner />}

      {showResults && (
        <SearchResults results={data.results} />
      )}

      {showError && (
        <ErrorMessage>Failed to fetch results</ErrorMessage>
      )}
    </div>
  );
}

应避免的反模式

❌ 不要:在渲染时创建新对象
// BAD: New object on every render causes unnecessary re-renders
function MyProvider({ children }) {
  const [state, setState] = useState(null);

  return (
    <MyContext.Provider value={{ state, setState }}>
      {children}
    </MyContext.Provider>
  );
}
// GOOD: Memoize the context value
function MyProvider({ children }) {
  const [state, setState] = useState(null);

  const value = useMemo(() => ({ state, setState }), [state]);

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}
❌ 不要:忘记 SWR 条件式数据获取
// BAD: Fetches even with empty input
function useSearch(query: string) {
  const { data } = useSWR(`/api/search?q=${query}`);
  return data;
}
// GOOD: Only fetch when query is valid
function useSearch(query: string) {
  const { data } = useSWR(
    query.trim().length >= 2 ? `/api/search?q=${query}` : null
  );
  return data;
}
❌ 不要:忽略防抖与加载状态的区别
// BAD: Only shows loading during network request
function Search() {
  const [input, setInput] = useState('');
  const debounced = useDebounce(input, 300);
  const { data, isLoading } = useSWR(`/api?q=${debounced}`);

  // Spinner disappears while debouncing!
  return <>{isLoading && <Spinner />}</>;
}
// GOOD: Show loading during both debounce and fetch
function Search() {
  const [input, setInput] = useState('');
  const debounced = useDebounce(input, 300);
  const { data, isLoading } = useSWR(`/api?q=${debounced}`);

  const isDebouncing = input !== debounced;
  const loading = isDebouncing || isLoading;

  return <>{loading && <Spinner />}</>;
}
❌ 不要:使用缺乏类型安全的字符串状态
// BAD: Easy to typo, no autocomplete
function Component() {
  const [status, setStatus] = useState('idel'); // Typo!

  if (status === 'idle') { // Won't match
    return <div>Ready</div>;
  }
}
// GOOD: Use discriminated union
type Status = 'idle' | 'loading' | 'success' | 'error';

function Component() {
  const [status, setStatus] = useState<Status>('idle');

  if (status === 'idle') { // Type-safe
    return <div>Ready</div>;
  }
}

最佳实践总结

  1. 条件式 SWR 键:当条件不满足时,使用 null 阻止数据获取
  2. 记忆化转换:对开销较大的数据转换使用 useMemo
  3. 稳定的回调:对上下文或依赖项中的函数使用 useCallback
  4. 记忆化上下文值:防止上下文使用者不必要地重新渲染
  5. 双重加载状态:分别跟踪防抖和网络加载状态
  6. 纯辅助函数:将逻辑提取到组件外,以便测试
  7. 类型安全的状态:在状态机中使用可辨识联合
  8. 显式状态转换:让状态变化清晰且可预测

相关模式

查看 react-state-machine 技能,了解更多高级 XState 状态机模式。查看 react-advanced 技能,了解 React 19 平台功能和渲染架构——React Compiler(它会改变对手动记忆化的需求)、并发渲染、Actions,以及大规模上下文优化。

参考资料

评论

支持 Markdown