ReactHooks模式Skill react-hooks-patterns

这个技能用于掌握和使用 React Hooks 的各种模式,包括 useState、useEffect、useContext、useMemo、useCallback 和自定义钩子,实现现代 React 前端应用中的状态管理、副作用处理、性能优化和可重用逻辑。关键词:React Hooks, 前端开发, 状态管理, 副作用, 性能优化, 自定义钩子。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

name: react-hooks-patterns user-invocable: false description: 当需要 React Hooks 模式时使用,包括 useState、useEffect、useContext、useMemo、useCallback 和自定义钩子。用于现代 React 开发。 allowed-tools:

  • Bash
  • Read

React Hooks 模式

掌握 React Hooks 以构建现代的功能性 React 组件。
这个技能涵盖了内置钩子、自定义钩子和高级模式,用于状态管理和副作用处理。

useState 钩子

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(prev => prev - 1);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

// 复杂状态
interface User {
  name: string;
  email: string;
}

function UserForm() {
  const [user, setUser] = useState<User>({
    name: '',
    email: ''
  });
  
  const updateField = (field: keyof User, value: string) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        value={user.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
    </form>
  );
}

useEffect 钩子

import { useEffect, useState } from 'react';

function DataFetcher({ userId }: { userId: number }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    let cancelled = false;
    
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const result = await response.json();
        
        if (!cancelled) {
          setData(result);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }
    
    fetchData();
    
    return () => {
      cancelled = true;
    };
  }, [userId]);
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;
  return <div>{JSON.stringify(data)}</div>;
}

useContext 钩子

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

interface Theme {
  mode: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<Theme | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [mode, setMode] = useState<'light' | 'dark'>('light');
  
  const toggleTheme = () => {
    setMode(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ mode, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return context;
}

function ThemedButton() {
  const { mode, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      当前模式: {mode}
    </button>
  );
}

useMemo 和 useCallback

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

function ExpensiveComponent({ items }: { items: number[] }) {
  const [filter, setFilter] = useState('');
  
  // 记忆化昂贵计算
  const filteredItems = useMemo(() => {
    console.log('过滤项目中...');
    return items.filter(item => 
      item.toString().includes(filter)
    );
  }, [items, filter]);
  
  // 记忆化回调函数
  const handleFilterChange = useCallback((value: string) => {
    setFilter(value);
  }, []);
  
  return (
    <div>
      <input 
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
      />
      <ItemList items={filteredItems} />
    </div>
  );
}

自定义钩子

// useLocalStorage 钩子
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
  
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = 
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue] as const;
}

// useDebounce 钩子
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用示例
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  useEffect(() => {
    if (debouncedSearchTerm) {
      // 执行搜索
      console.log('搜索:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);
  
  return (
    <input
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  );
}

useReducer 用于复杂状态

import { useReducer } from 'react';

interface State {
  count: number;
  history: number[];
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'RESET' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
        history: [...state.history, state.count + 1]
      };
    case 'DECREMENT':
      return {
        count: state.count - 1,
        history: [...state.history, state.count - 1]
      };
    case 'RESET':
      return { count: 0, history: [0] };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    history: [0]
  });

  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
      <p>历史: {state.history.join(', ')}</p>
    </div>
  );
}

// 复杂表单状态与 useReducer
interface FormState {
  values: {
    name: string;
    email: string;
    age: number;
  };
  errors: {
    name?: string;
    email?: string;
    age?: string;
  };
  touched: {
    name: boolean;
    email: boolean;
    age: boolean;
  };
  isSubmitting: boolean;
}

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string | number }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'SET_TOUCHED'; field: string }
  | { type: 'SUBMIT_START' }
  | { type: 'SUBMIT_SUCCESS' }
  | { type: 'SUBMIT_ERROR' }
  | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value }
      };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error }
      };
    case 'SET_TOUCHED':
      return {
        ...state,
        touched: { ...state.touched, [action.field]: true }
      };
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true };
    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false };
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false };
    case 'RESET':
      return {
        values: { name: '', email: '', age: 0 },
        errors: {},
        touched: { name: false, email: false, age: false },
        isSubmitting: false
      };
    default:
      return state;
  }
}

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, {
    values: { name: '', email: '', age: 0 },
    errors: {},
    touched: { name: false, email: false, age: false },
    isSubmitting: false
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_START' });
    try {
      await submitForm(state.values);
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR' });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={state.values.name}
        onChange={(e) => dispatch({
          type: 'SET_FIELD',
          field: 'name',
          value: e.target.value
        })}
        onBlur={() => dispatch({ type: 'SET_TOUCHED', field: 'name' })}
      />
      {state.touched.name && state.errors.name && (
        <span>{state.errors.name}</span>
      )}
      <button type="submit" disabled={state.isSubmitting}>
        提交
      </button>
    </form>
  );
}

useRef 钩子

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

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

// 存储可变值
function Timer() {
  const intervalRef = useRef<number | null>(null);
  const [count, setCount] = useState(0);

  const start = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  useEffect(() => {
    return () => {
      if (intervalRef.current !== null) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

// 跟踪前一个值
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>当前: {count}</p>
      <p>前一个: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

useLayoutEffect 用于 DOM 测量

import { useLayoutEffect, useRef, useState } from 'react';

// 在绘制前测量元素尺寸
function TooltipWithMeasurement() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const tooltipRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (tooltipRef.current) {
      const { height } = tooltipRef.current.getBoundingClientRect();
      setTooltipHeight(height);
    }
  }, []);

  return (
    <div>
      <div
        ref={tooltipRef}
        style={{
          position: 'absolute',
          top: `calc(100% + ${tooltipHeight}px)`
        }}
      >
        工具提示内容
      </div>
    </div>
  );
}

// 同步滚动位置
function SyncedScrollPanels() {
  const leftRef = useRef<HTMLDivElement>(null);
  const rightRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const left = leftRef.current;
    const right = rightRef.current;
    if (!left || !right) return;

    const syncScroll = (source: HTMLDivElement, target: HTMLDivElement) => {
      return () => {
        target.scrollTop = source.scrollTop;
      };
    };

    const leftHandler = syncScroll(left, right);
    const rightHandler = syncScroll(right, left);

    left.addEventListener('scroll', leftHandler);
    right.addEventListener('scroll', rightHandler);

    return () => {
      left.removeEventListener('scroll', leftHandler);
      right.removeEventListener('scroll', rightHandler);
    };
  }, []);

  return (
    <div style={{ display: 'flex' }}>
      <div ref={leftRef} style={{ overflow: 'auto', height: 300 }}>
        左侧面板内容
      </div>
      <div ref={rightRef} style={{ overflow: 'auto', height: 300 }}>
        右侧面板内容
      </div>
    </div>
  );
}

useImperativeHandle 与 forwardRef

import {
  useRef,
  useImperativeHandle,
  forwardRef,
  useState
} from 'react';

// 定义暴露的方法接口
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (time: number) => void;
}

interface VideoPlayerProps {
  src: string;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  (props, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);
    const [isPlaying, setIsPlaying] = useState(false);

    useImperativeHandle(ref, () => ({
      play: () => {
        videoRef.current?.play();
        setIsPlaying(true);
      },
      pause: () => {
        videoRef.current?.pause();
        setIsPlaying(false);
      },
      seek: (time: number) => {
        if (videoRef.current) {
          videoRef.current.currentTime = time;
        }
      }
    }), []);

    return (
      <div>
        <video ref={videoRef} src={props.src} />
        <p>状态: {isPlaying ? '播放中' : '已暂停'}</p>
      </div>
    );
  }
);

function ParentComponent() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="video.mp4" />
      <button onClick={() => playerRef.current?.play()}>
        播放
      </button>
      <button onClick={() => playerRef.current?.pause()}>
        暂停
      </button>
      <button onClick={() => playerRef.current?.seek(30)}>
        跳转到 30 秒
      </button>
    </div>
  );
}

// 具有自定义命令式方法的输入框
interface InputHandle {
  focus: () => void;
  clear: () => void;
  getValue: () => string;
}

const CustomInput = forwardRef<InputHandle, { placeholder?: string }>(
  (props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current?.focus();
      },
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = '';
        }
      },
      getValue: () => {
        return inputRef.current?.value || '';
      }
    }), []);

    return <input ref={inputRef} placeholder={props.placeholder} />;
  }
);

自定义钩子组合模式

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

// 将多个钩子组合在一起
function useAsync<T>(asyncFunction: () => Promise<T>) {
  const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
  const [value, setValue] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  const execute = useCallback(() => {
    setStatus('pending');
    setValue(null);
    setError(null);

    return asyncFunction()
      .then((response) => {
        setValue(response);
        setStatus('success');
      })
      .catch((error) => {
        setError(error);
        setStatus('error');
      });
  }, [asyncFunction]);

  return { execute, status, value, error };
}

// 将 useAsync 与其他钩子组合
function useFetch<T>(url: string) {
  const fetchData = useCallback(
    () => fetch(url).then((res) => res.json() as Promise<T>),
    [url]
  );

  const { execute, status, value, error } = useAsync<T>(fetchData);

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

  return { data: value, loading: status === 'pending', error };
}

// 组合多个自定义钩子的钩子
function useForm<T extends Record<string, any>>(initialValues: T) {
  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 [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = useCallback((field: keyof T, value: any) => {
    setValues((prev) => ({ ...prev, [field]: value }));
  }, []);

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

  const handleSubmit = useCallback(
    async (
      onSubmit: (values: T) => Promise<void>,
      validate?: (values: T) => Partial<Record<keyof T, string>>
    ) => {
      if (validate) {
        const validationErrors = validate(values);
        setErrors(validationErrors);
        if (Object.keys(validationErrors).length > 0) return;
      }

      setIsSubmitting(true);
      try {
        await onSubmit(values);
      } finally {
        setIsSubmitting(false);
      }
    },
    [values]
  );

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

  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  };
}

// 使用组合钩子
function UserProfileForm() {
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  } = useForm({
    name: '',
    email: '',
    bio: ''
  });

  const validate = (vals: typeof values) => {
    const errs: Partial<Record<keyof typeof values, string>> = {};
    if (!vals.name) errs.name = '姓名是必填项';
    if (!vals.email) errs.email = '邮箱是必填项';
    return errs;
  };

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        handleSubmit(
          async (vals) => {
            await saveProfile(vals);
          },
          validate
        );
      }}
    >
      <input
        value={values.name}
        onChange={(e) => handleChange('name', e.target.value)}
        onBlur={() => handleBlur('name')}
      />
      {touched.name && errors.name && <span>{errors.name}</span>}
      <button type="submit" disabled={isSubmitting}>
        保存
      </button>
      <button type="button" onClick={reset}>
        重置
      </button>
    </form>
  );
}

高级 useCallback 和 useMemo 优化

import { useState, useCallback, useMemo, memo } from 'react';

// 复杂记忆化场景
interface Item {
  id: number;
  name: string;
  category: string;
  price: number;
}

interface Props {
  items: Item[];
}

const ItemList = memo(({ items }: Props) => {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

function OptimizedShop() {
  const [items] = useState<Item[]>([
    { id: 1, name: 'Apple', category: 'fruit', price: 1.5 },
    { id: 2, name: 'Banana', category: 'fruit', price: 0.8 },
    { id: 3, name: 'Carrot', category: 'vegetable', price: 1.2 }
  ]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');

  // 记忆化过滤项目
  const filteredItems = useMemo(() => {
    return items.filter((item) => {
      const matchesSearch = item.name
        .toLowerCase()
        .includes(searchTerm.toLowerCase());
      const matchesCategory =
        selectedCategory === 'all' || item.category === selectedCategory;
      return matchesSearch && matchesCategory;
    });
  }, [items, searchTerm, selectedCategory]);

  // 记忆化排序项目
  const sortedItems = useMemo(() => {
    return [...filteredItems].sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name);
      }
      return a.price - b.price;
    });
  }, [filteredItems, sortBy]);

  // 记忆化类别列表
  const categories = useMemo(() => {
    const uniqueCategories = new Set(items.map((item) => item.category));
    return ['all', ...Array.from(uniqueCategories)];
  }, [items]);

  // 记忆化回调
  const handleSearch = useCallback((value: string) => {
    setSearchTerm(value);
  }, []);

  const handleCategoryChange = useCallback((category: string) => {
    setSelectedCategory(category);
  }, []);

  const handleSortChange = useCallback((sort: 'name' | 'price') => {
    setSortBy(sort);
  }, []);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="搜索项目..."
      />
      <select
        value={selectedCategory}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        {categories.map((cat) => (
          <option key={cat} value={cat}>
            {cat}
          </option>
        ))}
      </select>
      <select
        value={sortBy}
        onChange={(e) => handleSortChange(e.target.value as 'name' | 'price')}
      >
        <option value="name">按名称排序</option>
        <option value="price">按价格排序</option>
      </select>
      <ItemList items={sortedItems} />
    </div>
  );
}

// 工厂模式与 useCallback
function useEventCallback<T extends (...args: any[]) => any>(fn: T): T {
  const ref = useRef<T>(fn);

  useLayoutEffect(() => {
    ref.current = fn;
  });

  return useCallback(
    ((...args) => ref.current(...args)) as T,
    []
  );
}

// useEventCallback 的使用
function FormWithEventCallback() {
  const [count, setCount] = useState(0);

  // 这个回调总是访问最新的计数,但保持稳定引用
  const handleSubmit = useEventCallback(() => {
    console.log('当前计数:', count);
  });

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <ExpensiveChild onSubmit={handleSubmit} />
    </div>
  );
}

高级钩子模式

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

// useInterval - 声明式间隔钩子
function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef(callback);

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

  useEffect(() => {
    if (delay === null) return;

    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

function Clock() {
  const [time, setTime] = useState(new Date());

  useInterval(() => {
    setTime(new Date());
  }, 1000);

  return <div>{time.toLocaleTimeString()}</div>;
}

// useOnScreen - 检测元素是否可见
function useOnScreen(ref: React.RefObject<HTMLElement>) {
  const [isIntersecting, setIntersecting] = useState(false);

  useEffect(() => {
    if (!ref.current) return;

    const observer = new IntersectionObserver(([entry]) =>
      setIntersecting(entry.isIntersecting)
    );

    observer.observe(ref.current);
    return () => {
      observer.disconnect();
    };
  }, [ref]);

  return isIntersecting;
}

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const isVisible = useOnScreen(ref);

  return (
    <div ref={ref}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div>加载中...</div>
      )}
    </div>
  );
}

// useMediaQuery - 响应式设计钩子
function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }

    const listener = () => setMatches(media.matches);
    media.addEventListener('change', listener);

    return () => media.removeEventListener('change', listener);
  }, [matches, query]);

  return matches;
}

function ResponsiveComponent() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
  const isDesktop = useMediaQuery('(min-width: 1025px)');

  return (
    <div>
      {isMobile && <div>移动视图</div>}
      {isTablet && <div>平板视图</div>}
      {isDesktop && <div>桌面视图</div>}
    </div>
  );
}

// useClickOutside - 检测元素外点击
function useClickOutside(
  ref: React.RefObject<HTMLElement>,
  handler: (event: MouseEvent | TouchEvent) => void
) {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useClickOutside(ref, () => setIsOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>切换</button>
      {isOpen && <div>下拉内容</div>}
    </div>
  );
}

// useToggle - 布尔状态管理
function useToggle(initialValue = false): [boolean, () => void] {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

function ToggleExample() {
  const [isOn, toggle] = useToggle(false);

  return (
    <div>
      <p>开关是 {isOn ? '开' : '关'}</p>
      <button onClick={toggle}>切换</button>
    </div>
  );
}

// useArray - 数组操作钩子
function useArray<T>(initialValue: T[]) {
  const [array, setArray] = useState(initialValue);

  const push = useCallback((element: T) => {
    setArray((a) => [...a, element]);
  }, []);

  const filter = useCallback((callback: (item: T) => boolean) => {
    setArray((a) => a.filter(callback));
  }, []);

  const update = useCallback((index: number, newElement: T) => {
    setArray((a) => [
      ...a.slice(0, index),
      newElement,
      ...a.slice(index + 1)
    ]);
  }, []);

  const remove = useCallback((index: number) => {
    setArray((a) => [...a.slice(0, index), ...a.slice(index + 1)]);
  }, []);

  const clear = useCallback(() => {
    setArray([]);
  }, []);

  return { array, set: setArray, push, filter, update, remove, clear };
}

function TodoList() {
  const { array: todos, push, remove, update } = useArray<{
    id: number;
    text: string;
    completed: boolean;
  }>([]);

  const addTodo = (text: string) => {
    push({ id: Date.now(), text, completed: false });
  };

  const toggleTodo = (index: number) => {
    const todo = todos[index];
    update(index, { ...todo, completed: !todo.completed });
  };

  return (
    <div>
      {todos.map((todo, index) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(index)}
          />
          <span>{todo.text}</span>
          <button onClick={() => remove(index)}>删除</button>
        </div>
      ))}
    </div>
  );
}

何时使用此技能

使用 react-hooks-patterns 当您需要:

  • 使用功能性组件构建现代 React 应用程序
  • 使用 useState 和 useReducer 管理组件状态
  • 使用 useEffect 处理副作用
  • 使用 useContext 跨组件共享状态
  • 使用 useMemo 和 useCallback 优化性能
  • 使用自定义钩子创建可重用逻辑
  • 使用 useRef 访问 DOM 元素
  • 构建可维护的 React 应用程序
  • 遵循 React 最佳实践和模式

最佳实践

  • 当新状态依赖于先前状态时,使用功能性更新
  • 始终在 useEffect 返回函数中清理副作用
  • 在 useEffect 依赖数组中包含所有依赖项
  • 使用 useCallback 记忆化传递给子组件的函数
  • 仅对昂贵计算使用 useMemo,而非简单值
  • 创建自定义钩子以封装和重用状态逻辑
  • 对具有多个子值的复杂状态逻辑使用 useReducer
  • 将钩子保持在组件顶层,切勿在条件中
  • 为自定义钩子命名以 “use” 前缀,以符合规范和 linting
  • 使用 TypeScript 以获得类型安全和更好的开发体验
  • 通过创建专注的自定义钩子分离关注点
  • 对不触发重新渲染的值使用 useRef
  • 仅在测量 DOM 或防止闪烁时使用 useLayoutEffect
  • 使用 memo() 与接收回调属性的组件
  • 组合钩子以从简单行为构建更复杂行为
  • 谨慎使用 useImperativeHandle,优先使用声明式模式
  • 避免使用 useMemo 和 useCallback 进行过早优化
  • 保持依赖数组诚实,使用 ESLint exhaustive-deps 规则
  • 将复杂逻辑提取到自定义钩子中以提高可测试性
  • 使用 useContext 处理全局状态,而非属性钻取

常见陷阱

  • 忘记在 useEffect 数组中包含依赖项
  • 未清理副作用导致内存泄漏
  • 过度使用 useCallback 和 useMemo 导致过早优化
  • 在条件或循环中调用钩子(违反钩子规则)
  • 在 useEffect 中未正确处理异步操作
  • 在 useEffect 中错误地更新状态导致无限循环
  • 在渲染期间而非效果中修改 ref.current
  • 在回调中使用过时闭包而无适当依赖项
  • 需要时未使用 useState 的功能性更新
  • 在卸载的组件上设置状态
  • 在依赖数组中使用对象或数组字面量
  • 未记忆化在每次渲染时运行的昂贵计算
  • 混淆 useEffect 与 useLayoutEffect 的用例
  • 未记忆化回调导致不必要的重新渲染
  • 对应使用 ref 的值使用 useState
  • 未为事件监听器和订阅使用清理函数
  • 忘记 useEffect 在绘制后运行,而非之前
  • 创建难以重用的紧密耦合自定义钩子
  • 过早过度抽象自定义钩子
  • 忽略 ESLint 关于依赖数组的警告

资源

官方 React 文档

指南和最佳实践

TypeScript 资源

附加资源