Skip to content

React Hooks 入门指南

React Hooks 是 React 16.8 引入的新特性,它允许我们在函数组件中使用状态和其他 React 特性。本文将介绍 React Hooks 的核心概念和使用方法。

1. useState

useState 是最基本的 Hook,用于在函数组件中添加状态。

javascript
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2. useEffect

useEffect 用于处理副作用,如数据获取、订阅或手动修改 DOM。

javascript
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `You clicked ${count} times`;

    // 清理函数,类似于 componentWillUnmount
    return () => {
      // 执行清理操作
    };
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

3. useContext

useContext 用于访问 React 的 Context API。

javascript
import React, { useContext } from 'react';

// 创建 Context
const ThemeContext = React.createContext('light');

function ThemedButton() {
  // 使用 Context
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>
      Themed Button
    </button>
  );
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

4. useReducer

useReduceruseState 的替代方案,用于处理复杂的状态逻辑。

javascript
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>
        Increment
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>
        Decrement
      </button>
    </div>
  );
}

5. useCallback

useCallback 用于缓存函数,避免不必要的重新渲染。

javascript
import React, { useState, useCallback } from 'react';

function ChildComponent({ onClick }) {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);

  // 缓存 onClick 函数
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 空依赖数组表示只创建一次

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

6. useMemo

useMemo 用于缓存计算结果,避免不必要的重新计算。

javascript
import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ number }) {
  // 缓存计算结果
  const result = useMemo(() => {
    console.log('Calculating...');
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
      sum += i;
    }
    return sum + number;
  }, [number]); // 只有当 number 改变时才重新计算

  return <p>Result: {result}</p>;
}

function App() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment Count
      </button>
      <p>Number: {number}</p>
      <button onClick={() => setNumber(number + 1)}>
        Increment Number
      </button>
      <ExpensiveCalculation number={number} />
    </div>
  );
}

7. useRef

useRef 用于创建一个可变的 ref 对象,用于访问 DOM 元素或存储任意值。

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

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const [count, setCount] = useState(0);

  const focusInput = () => {
    // 访问 DOM 元素
    inputEl.current.focus();
  };

  // 存储任意值
  const previousCountRef = useRef();

  useEffect(() => {
    previousCountRef.current = count;
  });

  const previousCount = previousCountRef.current;

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={focusInput}>Focus the input</button>
      <p>Current count: {count}</p>
      <p>Previous count: {previousCount}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

8. useImperativeHandle

useImperativeHandle 用于自定义暴露给父组件的实例值。

javascript
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // 自定义暴露给父组件的方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

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

function ParentComponent() {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={() => fancyInputRef.current.focus()}>
        Focus Input
      </button>
      <button onClick={() => fancyInputRef.current.clear()}>
        Clear Input
      </button>
    </div>
  );
}

9. useLayoutEffect

useLayoutEffectuseEffect 类似,但它在所有 DOM 变更之后同步执行。

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

function MeasureExample() {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const ref = useRef();

  useLayoutEffect(() => {
    setWidth(ref.current.offsetWidth);
    setHeight(ref.current.offsetHeight);
  });

  return (
    <div>
      <div ref={ref} style={{ width: '200px', height: '200px', background: 'red' }}>
        Measure me
      </div>
      <p>Width: {width}px</p>
      <p>Height: {height}px</p>
    </div>
  );
}

10. useDebugValue

useDebugValue 用于在 React DevTools 中显示自定义 Hook 的标签。

javascript
import { useDebugValue, useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // 模拟好友状态检查
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // 假设这是一个真实的 API
    // ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    // return () => {
    //   ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    // };

    // 模拟异步操作
    setTimeout(() => {
      setIsOnline(Math.random() > 0.5);
    }, 1000);
  }, [friendID]);

  // 在 DevTools 中显示自定义标签
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

function FriendListItem({ friend }) {
  const isOnline = useFriendStatus(friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {friend.name}
    </li>
  );
}

11. 自定义 Hook

我们可以创建自定义 Hook 来复用状态逻辑。

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

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

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

function App() {
  const { width, height } = useWindowSize();

  return (
    <div>
      <p>Window width: {width}</p>
      <p>Window height: {height}</p>
    </div>
  );
}

12. 最佳实践

  1. 只在函数顶部调用 Hook:不要在条件语句、循环或嵌套函数中调用 Hook
  2. 只在 React 函数组件中调用 Hook:不要在普通 JavaScript 函数中调用 Hook
  3. 使用 ESLint 插件:使用 eslint-plugin-react-hooks 来检查 Hook 的使用规则
  4. 合理使用依赖数组:确保 useEffectuseCallbackuseMemo 的依赖数组正确
  5. 避免过度使用 Hook:不要为了使用 Hook 而使用 Hook,保持代码简洁
  6. 使用自定义 Hook 复用逻辑:将重复的状态逻辑提取到自定义 Hook 中
  7. 注意性能优化:使用 useCallbackuseMemo 来避免不必要的重新渲染和计算

13. 常见问题

无限循环

如果 useEffect 的依赖数组不正确,可能会导致无限循环。

javascript
// 错误:count 是依赖项,每次更新都会重新执行
useEffect(() => {
  setCount(count + 1);
}, [count]);

// 正确:使用函数式更新
useEffect(() => {
  setCount(prevCount => prevCount + 1);
}, []);

闭包陷阱

由于闭包的存在,useEffect 可能会捕获到旧的状态值。

javascript
// 错误:count 总是 0
const [count, setCount] = useState(0);

useEffect(() => {
  setInterval(() => {
    console.log(count); // 总是 0
  }, 1000);
}, []);

// 正确:使用 ref 或函数式更新
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count;
});

useEffect(() => {
  setInterval(() => {
    console.log(countRef.current); // 正确的 count 值
  }, 1000);
}, []);

依赖数组遗漏

如果 useEffect 中使用了某个变量,但没有将其添加到依赖数组中,可能会导致 bug。

javascript
// 错误:遗漏了依赖项 name
const [name, setName] = useState('');
const [greeting, setGreeting] = useState('');

useEffect(() => {
  setGreeting(`Hello, ${name}!`);
}, []); // 遗漏了 name

// 正确:添加所有依赖项
useEffect(() => {
  setGreeting(`Hello, ${name}!`);
}, [name]);

14. 性能优化技巧

  1. 使用 React.memo:缓存组件,避免不必要的重新渲染
  2. 使用 useCallback:缓存函数,避免子组件不必要的重新渲染
  3. 使用 useMemo:缓存计算结果,避免不必要的重新计算
  4. 使用 useRef:存储不需要触发重新渲染的值
  5. 合理设置依赖数组:只在必要时重新执行副作用
  6. 使用 React.lazySuspense:实现组件懒加载

15. 总结

React Hooks 为函数组件带来了状态管理、副作用处理等能力,使我们能够编写更加简洁、可维护的代码。通过合理使用各种 Hook,我们可以构建更加复杂、功能强大的 React 应用。

掌握 React Hooks 是现代 React 开发的必备技能,希望本文能够帮助你快速入门 React Hooks,并在实际项目中灵活运用它们。