zeng

React Hooks完全指南

React Hooks 是 React 16.8 版本引入的新特性,它允许你在不编写类组件的情况下使用状态和其他 React 特性。Hooks 的出现极大地简化了 React 组件的编写和理解,成为现代 React 开发的标准方式。

为什么使用 Hooks

代码复用

Hooks 允许你将组件逻辑提取到可重用的函数中,避免了高阶组件和渲染属性等复杂模式。

更简洁的代码

使用 Hooks 可以编写更简洁、更易于理解的代码,减少模板代码的数量。

更好的类型支持

Hooks 与 TypeScript 有很好的兼容性,提供了更好的类型推断和检查。

改进的性能

Hooks 可以帮助你优化组件性能,避免不必要的重新渲染。

常用的 React Hooks

1. useState

useState钩子允许你在函数组件中使用状态:

import { useState } from "react";

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

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

2. useEffect

useEffect钩子允许你在函数组件中执行副作用操作,如数据获取、订阅或手动 DOM 操作:

import { useState, useEffect } from "react";

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 数据获取操作
    fetch("https://api.example.com/data")
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      })
      .catch((error) => {
        console.error("Error fetching data:", error);
        setLoading(false);
      });
  }, []); // 空依赖数组表示只在组件挂载时执行一次

  if (loading) {
    return <div>Loading...</div>;
  }

  return <div>Data: {JSON.stringify(data)}</div>;
}

3. useContext

useContext钩子允许你访问 React 上下文,避免了通过 props 层层传递数据:

import { createContext, useContext } from "react";

// 创建上下文
const ThemeContext = createContext("light");

// 使用上下文
function ThemeButton() {
  const theme = useContext(ThemeContext);

  return (
    <button
      style={{
        backgroundColor: theme === "dark" ? "#333" : "#fff",
        color: theme === "dark" ? "#fff" : "#333",
      }}
    >
      {theme} Theme
    </button>
  );
}

// 提供上下文
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemeButton />
    </ThemeContext.Provider>
  );
}

4. useReducer

useReducer钩子是useState的替代方案,用于管理复杂的状态逻辑:

import { useReducer } from "react";

// 定义reducer函数
function counterReducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  // 使用useReducer
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </div>
  );
}

5. useCallback

useCallback钩子返回一个记忆化的回调函数,避免不必要的重新渲染:

import { useState, useCallback } from "react";

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

  // 使用useCallback记忆化回调函数
  const handleClick = useCallback(() => {
    console.log("Button clicked");
  }, []); // 空依赖数组表示回调函数不会变化

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

function ChildComponent({ onClick }) {
  return <button onClick={onClick}>Child Button</button>;
}

6. useMemo

useMemo钩子返回一个记忆化的值,避免不必要的重复计算:

import { useState, useMemo } from "react";

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([1, 2, 3, 4, 5]);

  // 使用useMemo记忆化计算结果
  const sum = useMemo(() => {
    console.log("Calculating sum...");
    return items.reduce((total, item) => total + item, 0);
  }, [items]); // 只有当items变化时才重新计算

  return (
    <div>
      <p>Count: {count}</p>
      <p>Sum: {sum}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setItems([...items, items.length + 1])}>
        Add Item
      </button>
    </div>
  );
}

7. useRef

useRef钩子返回一个可变的 ref 对象,其.current 属性可以保存任何值:

import { useRef, useEffect } from "react";

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦输入框
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Enter text..." />
    </div>
  );
}

8. useImperativeHandle

useImperativeHandle钩子允许你自定义暴露给父组件的实例值:

import { forwardRef, useImperativeHandle } from "react";

const CustomInput = forwardRef((props, ref) => {
  const internalRef = useRef(null);

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

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

function ParentComponent() {
  const inputRef = useRef(null);

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Enter text..." />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <button onClick={() => inputRef.current.clear()}>Clear</button>
    </div>
  );
}

9. useLayoutEffect

useLayoutEffect钩子与useEffect类似,但它会在所有 DOM 变更之后同步执行,用于读取 DOM 布局并同步触发重渲染:

import { useState, useLayoutEffect } from "react";

function MeasuredComponent() {
  const [width, setWidth] = useState(0);
  const divRef = useRef(null);

  useLayoutEffect(() => {
    // 读取DOM布局
    const { offsetWidth } = divRef.current;
    setWidth(offsetWidth);
  }, []);

  return (
    <div ref={divRef}>
      <p>Width: {width}px</p>
    </div>
  );
}

10. useDebugValue

useDebugValue钩子用于在 React 开发者工具中显示自定义 Hook 的标签:

import { useState, useDebugValue } from "react";

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

  // 在React开发者工具中显示自定义Hook的标签
  useDebugValue(value > 0 ? "Positive" : "Negative");

  return [value, setValue];
}

Hooks 的规则

使用 Hooks 时需要遵循以下规则:

1. 只在顶层调用 Hooks

不要在循环、条件或嵌套函数中调用 Hooks:

// 错误的做法
if (count > 0) {
  useState(count);
}

// 正确的做法
const [value, setValue] = useState(count > 0 ? count : 0);

2. 只在 React 函数中调用 Hooks

Hooks 只能在函数组件或自定义 Hook 中使用:

// 错误的做法
function regularFunction() {
  useState("value");
}

// 正确的做法
function FunctionComponent() {
  const [value, setValue] = useState("value");
  return <div>{value}</div>;
}

3. 使用 ESLint 插件

使用eslint-plugin-react-hooks插件可以帮助你遵守 Hooks 的规则:

npm install eslint-plugin-react-hooks --save-dev

.eslintrc.js中配置:

module.exports = {
  plugins: ["react-hooks"],
  rules: {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
  },
};

自定义 Hooks

你可以创建自己的自定义 Hooks 来封装可重用的组件逻辑:

import { useState, useEffect } from "react";

// 自定义Hook:useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("Network response was not ok");
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (error) {
        setError(error.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// 使用自定义Hook
function DataComponent() {
  const { data, loading, error } = useFetch("https://api.example.com/data");

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return <div>Data: {JSON.stringify(data)}</div>;
}

Hooks 与类组件的比较

特性类组件函数组件 + Hooks
状态管理this.state 和 this.setStateuseState
生命周期方法componentDidMount, componentDidUpdate, componentWillUnmountuseEffect
上下文this.contextuseContext
引用createRefuseRef
代码复用高阶组件、渲染属性自定义 Hooks
性能优化shouldComponentUpdate, PureComponentuseCallback, useMemo, React.memo

总结

React Hooks 是 React 开发的重要里程碑,它简化了组件的编写和理解,提供了更好的代码复用和性能优化能力。通过学习和掌握常用的 React Hooks,以及自定义 Hooks 的创建和使用,你可以编写更加简洁、高效和可维护的 React 组件。

随着 React 生态系统的不断发展,Hooks 已经成为现代 React 开发的标准方式,建议你在新项目中使用 Hooks,并逐步将旧项目迁移到 Hooks。