react-hooks-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Hooks Patterns Skill

React Hooks 模式技能

Overview

概述

Master React Hooks patterns including built-in hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo) and custom hook development for reusable logic.
掌握React Hooks模式,包括内置Hooks(useState、useEffect、useContext、useReducer、useCallback、useMemo)以及用于可复用逻辑的自定义Hook开发。

Learning Objectives

学习目标

  • Understand all built-in React Hooks
  • Create custom hooks for code reuse
  • Implement advanced hook patterns
  • Optimize performance with hooks
  • Handle side effects effectively
  • 理解所有内置React Hooks
  • 创建用于代码复用的自定义Hooks
  • 实现高级Hook模式
  • 使用Hooks优化性能
  • 有效处理副作用

Core Hooks

核心Hooks

useState

useState

jsx
const [state, setState] = useState(initialValue);

// Lazy initialization for expensive computations
const [state, setState] = useState(() => expensiveComputation());

// Functional updates when state depends on previous value
setState(prev => prev + 1);
jsx
const [state, setState] = useState(initialValue);

// 延迟初始化,适用于昂贵的计算
const [state, setState] = useState(() => expensiveComputation());

// 当状态依赖于前一个值时使用函数式更新
setState(prev => prev + 1);

useEffect

useEffect

jsx
useEffect(() => {
  // Side effect code
  const subscription = api.subscribe();

  // Cleanup function
  return () => {
    subscription.unsubscribe();
  };
}, [dependencies]); // Dependency array
jsx
useEffect(() => {
  // 副作用代码
  const subscription = api.subscribe();

  // 清理函数
  return () => {
    subscription.unsubscribe();
  };
}, [dependencies]); // 依赖数组

useContext

useContext

jsx
const value = useContext(MyContext);

// Best practice: Create custom hook
function useMyContext() {
  const context = useContext(MyContext);
  if (!context) {
    throw new Error('useMyContext must be within Provider');
  }
  return context;
}
jsx
const value = useContext(MyContext);

// 最佳实践:创建自定义Hook
function useMyContext() {
  const context = useContext(MyContext);
  if (!context) {
    throw new Error('useMyContext必须在Provider内部使用');
  }
  return context;
}

useReducer

useReducer

jsx
const [state, dispatch] = useReducer(reducer, initialState);

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
jsx
const [state, dispatch] = useReducer(reducer, initialState);

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

useCallback

useCallback

jsx
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // Dependencies
);
jsx
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // 依赖项
);

useMemo

useMemo

jsx
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);
jsx
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);

useRef

useRef

jsx
const ref = useRef(initialValue);

// DOM access
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();

// Mutable value that doesn't trigger re-render
const countRef = useRef(0);
jsx
const ref = useRef(initialValue);

// DOM访问
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();

// 可变值,不会触发重新渲染
const countRef = useRef(0);

Custom Hooks Library

自定义Hook库

useFetch

useFetch

jsx
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}
jsx
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

useLocalStorage

useLocalStorage

jsx
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setValue = (value) => {
    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];
}
jsx
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setValue = (value) => {
    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];
}

useDebounce

useDebounce

jsx
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
}
jsx
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
}

usePrevious

usePrevious

jsx
function usePrevious(value) {
  const ref = useRef();

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

  return ref.current;
}
jsx
function usePrevious(value) {
  const ref = useRef();

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

  return ref.current;
}

useToggle

useToggle

jsx
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}
jsx
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}

useOnClickOutside

useOnClickOutside

jsx
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

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

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}
jsx
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

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

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

Practice Exercises

练习任务

  1. Counter with useReducer: Build a counter using useReducer instead of useState
  2. Theme Context: Create a theme switcher using useContext
  3. Data Fetching: Implement useFetch with caching
  4. Form Handler: Create useForm custom hook for form state management
  5. Window Size Hook: Build useWindowSize hook for responsive layouts
  6. Intersection Observer: Create useInView hook for lazy loading
  7. Timer Hook: Build useInterval and useTimeout hooks
  1. 使用useReducer实现计数器:用useReducer替代useState构建计数器
  2. 主题上下文:使用useContext创建主题切换器
  3. 数据获取:实现带缓存的useFetch
  4. 表单处理器:创建用于表单状态管理的useForm自定义Hook
  5. 窗口大小Hook:构建用于响应式布局的useWindowSize Hook
  6. 交叉观察器:创建用于懒加载的useInView Hook
  7. 计时器Hook:构建useInterval和useTimeout Hook

Common Patterns

常见模式

Fetching with Abort

可中断的请求获取

jsx
function useFetchWithAbort(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const abortController = new AbortController();

    fetch(url, { signal: abortController.signal })
      .then(res => res.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err });
        }
      });

    return () => abortController.abort();
  }, [url]);

  return state;
}
jsx
function useFetchWithAbort(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const abortController = new AbortController();

    fetch(url, { signal: abortController.signal })
      .then(res => res.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err });
        }
      });

    return () => abortController.abort();
  }, [url]);

  return state;
}

Combining Multiple Hooks

组合多个Hooks

jsx
function useAuthenticatedApi(url) {
  const { token } = useAuth();
  const { data, loading, error } = useFetch(url, {
    headers: {
      Authorization: `Bearer ${token}`
    }
  });

  return { data, loading, error };
}
jsx
function useAuthenticatedApi(url) {
  const { token } = useAuth();
  const { data, loading, error } = useFetch(url, {
    headers: {
      Authorization: `Bearer ${token}`
    }
  });

  return { data, loading, error };
}

Best Practices

最佳实践

  1. Name custom hooks with "use" prefix
  2. Extract reusable logic into custom hooks
  3. Keep hooks at component top level (no conditionals)
  4. Use ESLint plugin for hooks rules
  5. Document custom hook dependencies
  6. Test custom hooks with @testing-library/react-hooks
  1. 自定义Hook以"use"为前缀命名
  2. 将可复用逻辑提取到自定义Hook中
  3. 将Hooks放在组件顶层(不要在条件语句中使用)
  4. 使用ESLint插件检查Hooks规则
  5. 记录自定义Hook的依赖项
  6. 使用@testing-library/react-hooks测试自定义Hook

Resources

参考资源

Assessment

自我评估

Can you:
  • Explain when to use each built-in hook?
  • Create custom hooks for reusable logic?
  • Handle cleanup in useEffect properly?
  • Optimize with useCallback and useMemo?
  • Manage complex state with useReducer?
  • Build a custom hook library for your project?

你是否能够:
  • 解释何时使用每个内置Hook?
  • 创建用于可复用逻辑的自定义Hook?
  • 正确处理useEffect中的清理操作?
  • 使用useCallback和useMemo进行优化?
  • 使用useReducer管理复杂状态?
  • 为项目构建自定义Hook库?

Unit Test Template

单元测试模板

jsx
// __tests__/useCustomHook.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import { useCustomHook } from '../useCustomHook';

describe('useCustomHook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should initialize with default state', () => {
    const { result } = renderHook(() => useCustomHook());
    expect(result.current.state).toBe(initialValue);
  });

  it('should handle async operations', async () => {
    const { result } = renderHook(() => useCustomHook());

    await act(async () => {
      await result.current.asyncAction();
    });

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
  });

  it('should cleanup on unmount', () => {
    const cleanup = jest.fn();
    const { unmount } = renderHook(() => useCustomHook({ onCleanup: cleanup }));

    unmount();
    expect(cleanup).toHaveBeenCalled();
  });
});
jsx
// __tests__/useCustomHook.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import { useCustomHook } from '../useCustomHook';

describe('useCustomHook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should initialize with default state', () => {
    const { result } = renderHook(() => useCustomHook());
    expect(result.current.state).toBe(initialValue);
  });

  it('should handle async operations', async () => {
    const { result } = renderHook(() => useCustomHook());

    await act(async () => {
      await result.current.asyncAction();
    });

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
  });

  it('should cleanup on unmount', () => {
    const cleanup = jest.fn();
    const { unmount } = renderHook(() => useCustomHook({ onCleanup: cleanup }));

    unmount();
    expect(cleanup).toHaveBeenCalled();
  });
});

Retry Logic Pattern

重试逻辑模式

jsx
function useRetryAsync(asyncFn, options = {}) {
  const { maxRetries = 3, backoff = 1000 } = options;
  const [state, setState] = useState({ data: null, error: null, loading: false });

  const execute = useCallback(async (...args) => {
    setState(s => ({ ...s, loading: true, error: null }));
    let lastError;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const data = await asyncFn(...args);
        setState({ data, error: null, loading: false });
        return data;
      } catch (error) {
        lastError = error;
        if (attempt < maxRetries) {
          await new Promise(r => setTimeout(r, backoff * Math.pow(2, attempt)));
        }
      }
    }

    setState({ data: null, error: lastError, loading: false });
    throw lastError;
  }, [asyncFn, maxRetries, backoff]);

  return { ...state, execute };
}

Version: 2.0.0 Last Updated: 2025-12-30 SASMP Version: 2.0.0 Difficulty: Intermediate Estimated Time: 2-3 weeks Prerequisites: React Fundamentals Changelog: Added retry patterns, test templates, and observability hooks
jsx
function useRetryAsync(asyncFn, options = {}) {
  const { maxRetries = 3, backoff = 1000 } = options;
  const [state, setState] = useState({ data: null, error: null, loading: false });

  const execute = useCallback(async (...args) => {
    setState(s => ({ ...s, loading: true, error: null }));
    let lastError;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const data = await asyncFn(...args);
        setState({ data, error: null, loading: false });
        return data;
      } catch (error) {
        lastError = error;
        if (attempt < maxRetries) {
          await new Promise(r => setTimeout(r, backoff * Math.pow(2, attempt)));
        }
      }
    }

    setState({ data: null, error: lastError, loading: false });
    throw lastError;
  }, [asyncFn, maxRetries, backoff]);

  return { ...state, execute };
}

版本: 2.0.0 最后更新: 2025-12-30 SASMP版本: 2.0.0 难度: 中级 预计学习时间: 2-3周 前置知识: React基础 更新日志: 新增重试模式、测试模板和可观察性Hook