关于 react-hooks-composition
react-hooks-composition 是面向 React 和 TypeScript 开发者的自定义 Hooks 组合模式指南,涵盖 SWR 数据获取、搜索防抖、上下文优化和状态机。
主要功能
- 01SWR 条件式数据获取
- 02防抖搜索与双重加载状态
- 03记忆化 Context Provider
- 04类型安全的状态机
- 05纯辅助函数便于测试
适用场景
- 01按用户输入条件请求数据
- 02减少搜索输入时的 API 请求
- 03避免 Context 消费者不必要重渲染
- 04管理异步操作的加载与错误状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill react-hooks-composition安装后在 Claude Code 中输入/react-hooks-composition确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.jsonreferences/performance-optimization.mdswr-patterns.mdtesting-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>;
}
}
最佳实践总结
- 条件式 SWR 键:当条件不满足时,使用
null阻止数据获取 - 记忆化转换:对开销较大的数据转换使用
useMemo - 稳定的回调:对上下文或依赖项中的函数使用
useCallback - 记忆化上下文值:防止上下文使用者不必要地重新渲染
- 双重加载状态:分别跟踪防抖和网络加载状态
- 纯辅助函数:将逻辑提取到组件外,以便测试
- 类型安全的状态:在状态机中使用可辨识联合
- 显式状态转换:让状态变化清晰且可预测
相关模式
查看 react-state-machine 技能,了解更多高级 XState 状态机模式。查看 react-advanced 技能,了解 React 19 平台功能和渲染架构——React Compiler(它会改变对手动记忆化的需求)、并发渲染、Actions,以及大规模上下文优化。
参考资料
评论
支持 Markdown