React自定义 Hooks 的入门

305 阅读4分钟

React 的自定义 Hooks 是一个让代码更加简洁、逻辑更易复用的强大工具。但对于刚接触它的新手来说,自定义 Hooks 的实际作用和使用场景可能还不够清晰。本篇文章将从零开始,结合实例,带你一步步理解自定义 Hooks 的核心目标,并展示它在状态逻辑和行为逻辑中的应用。

一、自定义 Hooks 是什么?

自定义 Hooks 是基于 React 原生 Hooks 的扩展,允许我们将常用的逻辑封装起来,并在多个组件中复用。它的名字通常以 use 开头,比如 useCounter 或 useFetch。

自定义 Hooks 主要有两大核心功能:

  1. 状态逻辑的复用:封装状态及其操作逻辑,减少重复代码。
  2. 行为逻辑的复用:封装事件监听、API 请求等副作用逻辑,统一管理复杂的组件行为。

二、为什么要用自定义 Hooks?

在开发中,如果我们发现某些逻辑在多个组件中重复出现,就可以将它抽取到一个自定义 Hook 中。这样不仅可以提高代码的可维护性,还能让组件更加专注于 UI 渲染。

自定义 Hooks 的优势:

  • 逻辑抽离:将状态和行为逻辑从组件中抽离,保持组件的单一职责。
  • 提高复用性:避免重复编写相似代码。
  • 易于测试:自定义 Hooks 可以独立测试,保证逻辑的正确性。

三、状态逻辑复用的实践

状态逻辑复用是自定义 Hooks 的基础场景,它帮助我们管理状态及其相关操作。

示例 1:封装计数器逻辑

以下是一个简单的计数器自定义 Hook:

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue); 
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1); 

  return { count, increment, decrement };
} 

// 使用方式
function CounterA() {
  const { count, increment } = useCounter();
  return <button onClick={increment}>Counter A: {count}</button>;
} 

function CounterB() {
  const { count, decrement } = useCounter();
  return <button onClick={decrement}>Counter B: {count}</button>;
}

示例 2:封装表单输入逻辑

表单输入是常见的状态逻辑,通过自定义 Hook,我们可以快速管理输入值及其事件处理器。

function useInput(initialValue = '') {
  const [value, setValue] = useState(initialValue);

  const handleChange = (e) => setValue(e.target.value);

  return { value, onChange: handleChange, setValue };
}

// 使用方式
function InputField() {
  const name = useInput('');
  const email = useInput('');

  return (
    <div>
      <input {...name} placeholder="Name" />
      <input {...email} placeholder="Email" />
      <p>Name: {name.value}</p>
      <p>Email: {email.value}</p>
    </div>
  );
}

四、行为逻辑复用的实践

行为逻辑复用更多地涉及事件处理和副作用逻辑,比如监听窗口大小、滚动位置,或封装防抖逻辑等。

示例 1:封装窗口大小监听逻辑

function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 使用方式
function Component() {
  const { width, height } = useWindowSize();
  return <p>Window size: {width} x {height}</p>;
}

示例 2:封装防抖逻辑

function useDebouncedCallback(callback, delay) {
  const timeoutRef = useRef();

  const debouncedCallback = (...args) => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => callback(...args), delay);
  };

  return debouncedCallback;
}

// 使用方式
function SearchInput() {
  const [query, setQuery] = useState('');
  const handleSearch = useDebouncedCallback((value) => {
    console.log('Search:', value);
  }, 500);

  const handleChange = (e) => {
    setQuery(e.target.value);
    handleSearch(e.target.value);
  };

  return <input value={query} onChange={handleChange} placeholder="Search..." />;
}

五、状态与行为逻辑的结合

在实际开发中,状态和行为往往是紧密结合的。自定义 Hooks 允许我们将它们一并封装起来。

示例:数据请求的封装

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

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((error) => setError(error))
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// 使用方式
function Component() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

六、自定义 Hooks 的测试

测试自定义 Hooks 是确保其逻辑正确性的重要一步。以下是常见的测试工具和示例。

使用 @testing-library/react-hooks

@testing-library/react-hooks 是专门用于测试 React Hooks 的工具。

示例:测试 useCounter

import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter'; // 引入你的 Hook

test('should increment and decrement count', () => {
  const { result } = renderHook(() => useCounter(0));

  // 初始值
  expect(result.current.count).toBe(0);

  // 调用 increment
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);

  // 调用 decrement
  act(() => {
    result.current.decrement();
  });
  expect(result.current.count).toBe(0);
});

示例:测试 useFetch

import { renderHook } from '@testing-library/react-hooks';
import useFetch from './useFetch';

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: 'test' }),
  })
);

test('should fetch data successfully', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useFetch('https://api.example.com'));

  // 初始状态
  expect(result.current.loading).toBe(true);

  // 等待数据更新
  await waitForNextUpdate();

  expect(result.current.loading).toBe(false);
  expect(result.current.data).toEqual({ data: 'test' });
  expect(result.current.error).toBe(null);
});

小提示:在测试自定义 Hooks 时,要特别注意副作用的处理,可以通过 jest.fn() 模拟外部依赖。

七、总结

本文从自定义 Hooks 的基础概念出发,逐步展示了状态逻辑、行为逻辑及其结合的实践,并补充了自定义 Hooks 的测试方法。无论是刚接触还是希望深入了解自定义 Hooks,希望这篇文章能为你带来启发。尝试从你的项目中封装一个自定义 Hook,充分体验它带来的便利与提升吧!🚀