深入理解React Hooks

308 阅读18分钟

深入理解 React Hooks 对于现代 React 开发至关重要。自从 React 16.8 引入 Hooks 以来,函数组件已成为构建复杂应用的主流选择。Hooks 不仅简化了状态和副作用的管理,还提高了代码的可复用性和可维护性。本文将全面解析 React Hooks,包括其基础概念、核心 Hooks、定制 Hooks、与 Vue 3 Composition API 的对比、最佳实践以及常见误区,帮助你全面掌握并高效应用 Hooks 构建高质量的 React 应用。


目录

  1. React Hooks 概述
  1. 核心 Hooks
  1. 自定义 Hooks
  1. Hooks 模拟生命周期方法
  1. 与 Vue 3 Composition API 的对比
  1. Hooks 的最佳实践
  1. 常见问题与误区
  1. 示例与实践
  1. 总结

1. React Hooks 概述

1.1 什么是 Hooks

Hooks 是 React 16.8 引入的一组函数,允许你在函数组件中“钩入” React 的状态和生命周期功能。之前,这些功能只能在类组件中使用。Hooks 使得函数组件也能拥有管理状态、处理副作用等强大功能,促进了代码的简洁性和可复用性。

1.2 Hooks 的优势

  • 简洁性:通过函数组件和 Hooks,代码更加简洁和易读,减少了类组件中的样板代码。
  • 逻辑复用:自定义 Hooks 允许在多个组件之间复用状态逻辑,提升代码的可维护性。
  • 组合性:Hooks 可以组合使用,构建复杂的功能模块。
  • 性能优化:合理使用 Hooks(如 useMemouseCallback)可以优化渲染性能。
  • 更好的测试性:Hooks 逻辑可以更容易地进行单元测试。

1.3 使用 Hooks 的规则

为了确保 Hooks 的正确运行,React 定义了以下两条基本规则:

  1. 只在顶层调用 Hooks
    • 不要在循环、条件或嵌套函数中调用 Hooks。
    • 确保 Hooks 在组件的顶层执行,以保持 Hook 调用的顺序一致。
  1. 只在 React 函数组件或自定义 Hooks 中调用 Hooks
    • 不要在普通的 JavaScript 函数中调用 Hooks。
    • 只能在 React 函数组件或自定义的 Hook 函数中调用其他 Hooks。

违反这些规则可能导致不可预期的行为或错误。


2. 核心 Hooks

React 提供了一系列内置 Hooks,用于管理状态、处理副作用、引用 DOM 元素等。以下是一些核心 Hooks 的详细介绍。

2.1 useState

useState 是最基本的 Hook,用于在函数组件中声明和管理本地状态。

示例:

import React, { useState } from 'react';

function Counter() {
  // 声明一个名为 "count" 的新状态变量,初始值为 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数: {count}</p>

      <button onClick={() => setCount(count + 1)}>增加</button>

    </div>

  );
}

export default Counter;

解释:

  • useState 接受初始状态作为参数,返回一个数组,第一项为当前状态值,第二项为更新状态的函数。
  • 当调用 setCount 更新状态时,组件会重新渲染,并显示新的计数值。

2.2 useEffect

useEffect 用于在函数组件中处理副作用,如数据获取、订阅、手动操作 DOM 等。它可以模拟类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法。

2.2.1 基本用法

示例:

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

function FetchData({ url }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 副作用:数据获取
    fetch(url)
      .then(response => response.json())
      .then(data => setData(data));
  }, [url]); // 依赖项数组

  if (!data) return <p>加载中...</p>;

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

export default FetchData;

解释:

  • useEffect 在组件渲染后执行。
  • 依赖项数组 [url] 表示当 url 变化时重新执行副作用。
  • 初始挂载和依赖项变化都会触发副作用。
2.2.2 清理副作用

useEffect 可以返回一个清理函数,用于在组件卸载或在下一次副作用执行前清理上一次的副作用。

示例:

import React, { useEffect } from 'react';

function Timer() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // 清理函数
    return () => clearInterval(timer);
  }, []); // 空依赖项数组,仅在挂载和卸载时执行

  return <div>计时器正在运行。查看控制台以获取滴答信息。</div>;
}

export default Timer;

解释:

  • 副作用在组件挂载时执行,设置一个定时器。
  • 返回的清理函数在组件卸载时调用,清除定时器。
2.2.3 依赖项数组

依赖项数组决定了 useEffect 何时执行副作用:

  • 无依赖项:每次渲染后都执行副作用。
  • 空数组 []:仅在挂载和卸载时执行副作用。
  • 特定依赖项:仅在依赖项变化时执行副作用。

示例:

useEffect(() => {
  // 每次渲染后执行
});

useEffect(() => {
  // 仅在挂载和卸载时执行
}, []);

useEffect(() => {
  // 依赖项变化时执行
}, [dependency1, dependency2]);

2.3 useContext

useContext 用于在函数组件中消费 Context,从而在组件树中共享数据,无需通过 props 层层传递。

示例:

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

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

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#000' }}>
      主题按钮
    </button>

  );
}

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

  );
}

export default App;

解释:

  • createContext 创建一个 Context 对象。
  • ThemeContext.Provider 提供上下文值给子组件。
  • useContext Hook 在子组件中消费 Context 值。

2.4 useReducer

useReducer 是用于管理复杂状态逻辑的 Hook,适用于状态依赖多个子值或有复杂的状态更新逻辑的场景。

示例:

import React, { useReducer } from 'react';

const initialState = { count: 0 };

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, initialState);

  return (
    <div>
      <p>计数: {state.count}</p>

      <button onClick={() => dispatch({ type: 'increment' })}>增加</button>

      <button onClick={() => dispatch({ type: 'decrement' })}>减少</button>

    </div>

  );
}

export default Counter;

解释:

  • useReducer 接受一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。
  • dispatch 函数用于发送动作,触发状态更新。

2.5 useCallbackuseMemo

useCallbackuseMemo 是用于性能优化的 Hooks,帮助缓存函数和计算结果,避免不必要的重新创建和计算。

  • useCallback:返回一个 memoized 回调函数,适用于将回调函数传递给子组件,避免因函数引用变化导致的子组件重新渲染。示例:
import React, { useState, useCallback } from 'react';

const ExpensiveComponent = React.memo(({ onClick }) => {
  console.log('ExpensiveComponent 渲染');
  return <button onClick={onClick}>点击我</button>;
});

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

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>计数: {count}</p>

      <ExpensiveComponent onClick={handleClick} />
    </div>

  );
}

export default Parent;
  • useMemo:返回一个 memoized 的值,适用于缓存计算结果,避免在每次渲染时重复计算。示例:
import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ number }) {
  const computedValue = useMemo(() => {
    console.log('计算中...');
    let total = 0;
    for (let i = 0; i < 1000000; i++) {
      total += i;
    }
    return total + number;
  }, [number]);

  return <div>计算结果: {computedValue}</div>;
}

function App() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(false);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>增加计数</button>

      <button onClick={() => setOther(!other)}>切换其他状态</button>

      <ExpensiveCalculation number={count} />
    </div>

  );
}

export default App;

2.6 useRef

useRef 用于在函数组件中创建一个可变的引用,该引用在组件的整个生命周期内保持不变。常用于访问 DOM 元素或存储任何可变值。

示例:

import React, { useRef } from 'react';

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

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>

    </div>

  );
}

export default TextInput;

用途:

  • 访问 DOM 元素:如获取输入框的值或焦点。
  • 存储可变值:不引起重新渲染的变量,如定时器 ID。

2.7 useLayoutEffect

useLayoutEffectuseEffect 类似,但它会在所有 DOM 变更之后、浏览器绘制之前同步执行。这使得它适用于需要同步测量 DOM 或在 DOM 变更前进行某些操作的场景。

示例:

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

function MeasureDiv() {
  const divRef = useRef();

  useLayoutEffect(() => {
    const height = divRef.current.offsetHeight;
    console.log('Div 高度:', height);
  }, []);

  return <div ref={divRef} style={{ height: '100px', background: 'lightblue' }}>测量我的高度!</div>;
}

export default MeasureDiv;

注意:

  • useLayoutEffect 的执行时机可能会阻塞浏览器绘制,需谨慎使用。
  • 在大多数情况下,useEffect 足以满足需求,只有在需要同步读取布局时才使用 useLayoutEffect

2.8 useImperativeHandle

useImperativeHandleforwardRef 一起使用,允许你自定义暴露给父组件的实例值。常用于封装组件并提供对其内部 DOM 元素或方法的访问。

示例:

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

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

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} type="text" />;
});

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

  const handleFocus = () => {
    fancyInputRef.current.focus();
  };

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={handleFocus}>聚焦 FancyInput</button>

    </div>

  );
}

export default App;

解释:

  • forwardRef 转发 ref 到子组件内部。
  • useImperativeHandle 自定义暴露给父组件的实例值。

2.9 useDebugValue

useDebugValue 用于在 React DevTools 中显示自定义 Hooks 的标签,有助于调试。

示例:

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

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

  useDebugValue(isOnline ? '在线' : '离线');

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // 假设 subscribeToFriendStatus 是一个订阅好友状态的函数
    subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  }, [friendID]);

  return isOnline;
}

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

  return (
    <li style={{ color: isOnline ? 'green' : 'red' }}>
      {props.friend.name}
    </li>

  );
}

解释:

  • useDebugValue 在 React DevTools 中显示自定义 Hooks 的状态标签。
  • 仅用于开发环境,对生产环境没有影响。

3. 自定义 Hooks

自定义 Hooks 允许你将组件逻辑提取到可复用的函数中,增强代码的可复用性和可维护性。它们遵循命名约定,以 use 开头,并且可以组合现有的 Hooks 来实现复杂的逻辑。

3.1 创建自定义 Hooks

示例:

import { useState, useEffect } from 'react';

// 自定义 Hook:用于获取窗口宽度
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener('resize', handleResize);
    
    // 清理函数
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return width;
}

export default useWindowWidth;

解释:

  • 自定义 Hook useWindowWidth 封装了获取窗口宽度的逻辑。
  • 使用该 Hook 的组件可以轻松获取和响应窗口宽度的变化。

3.2 自定义 Hooks 的最佳实践

  • 命名约定:自定义 Hooks 的名称应以 use 开头,遵循驼峰命名法,如 useFetchuseAuth
  • 单一职责:每个 Hook 应专注于实现单一的功能,便于复用和测试。
  • 组合 Hooks:通过组合多个 Hooks,构建复杂的逻辑,提升代码的可复用性。
  • 避免在自定义 Hooks 中使用条件语句:保持 Hook 调用顺序的一致性,遵循 Hooks 的规则。

3.3 示例:数据获取自定义 Hook

示例:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let isMounted = true;

    async function fetchData() {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('网络响应错误');
        }
        const result = await response.json();
        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

解释:

  • useFetch Hook 封装了数据获取的逻辑,管理加载状态和错误状态。
  • 通过返回 { data, loading, error },组件可以轻松使用这些状态来渲染 UI。

使用示例:

import React from 'react';
import useFetch from './useFetch';

function DataDisplay({ url }) {
  const { data, loading, error } = useFetch(url);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

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

export default DataDisplay;

4. Hooks 模拟生命周期方法

通过 Hooks,函数组件可以模拟类组件的生命周期方法。理解这种模拟关系有助于更好地运用 Hooks 管理组件生命周期。

4.1 类组件生命周期方法与 Hooks 对比

生命周期阶段类组件生命周期方法函数组件 Hooks
挂载constructor componentDidMountuseState 初始化状态 useEffect(空依赖)
更新componentDidUpdateuseEffect(依赖项变化)
卸载componentWillUnmountuseEffect(清理函数)
错误componentDidCatch getDerivedStateFromError无直接对应 Hooks(使用错误边界)

4.2 Hooks 模拟生命周期方法的实现

4.2.1 componentDidMount vs useEffect(空依赖项数组)

类组件:

class MyComponent extends React.Component {
  componentDidMount() {
    // 执行副作用,如数据获取
  }

  render() {
    return <div>My Component</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 执行副作用,如数据获取
  }, []); // 空依赖项数组,类似 componentDidMount

  return <div>My Component</div>;
}

export default MyComponent;
4.2.2 componentDidUpdate vs useEffect(依赖项变化)

类组件:

class MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.value !== prevProps.value) {
      // 执行副作用,如更新数据
    }
  }

  render() {
    return <div>Value: {this.props.value}</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent({ value }) {
  useEffect(() => {
    // 执行副作用,如更新数据
  }, [value]); // 仅当 value 变化时执行

  return <div>Value: {value}</div>;
}

export default MyComponent;
4.2.3 componentWillUnmount vs useEffect(清理函数)

类组件:

class MyComponent extends React.Component {
  componentDidMount() {
    this.timer = setInterval(() => {
      // 定时任务
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timer); // 清理定时器
  }

  render() {
    return <div>Timer</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      // 定时任务
    }, 1000);

    // 清理函数
    return () => clearInterval(timer);
  }, []); // 空依赖项数组,仅在挂载和卸载时执行

  return <div>Timer</div>;
}

export default MyComponent;
4.2.4 错误处理

类组件:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

函数组件:

目前,函数组件无法直接捕获错误。需要使用类组件作为错误边界。

示例:

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

export default ErrorBoundary;

使用错误边界:

import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyComponent from './MyComponent';

function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>

  );
}

export default App;

注意: 未来 React 可能会引入 Hooks 来处理错误边界,但目前需要使用类组件。


5. 与 Vue 3 Composition API 的对比

对于熟悉 Vue 3 Composition API 的开发者而言,理解 React Hooks 与 Vue 3 Composition API 的异同,有助于更快地掌握 React 的核心概念和最佳实践。

5.1 基础概念对比

特性React HooksVue 3 Composition API
状态管理useStateuseReducerrefreactive
副作用管理useEffectuseLayoutEffectwatchonMountedonUnmounted
上下文管理useContextprovideinject
引用 DOM 元素useRefref(用于模板中的 DOM 元素)
逻辑复用自定义 HooksComposition API 中的函数组合
错误处理错误边界(需要类组件)errorCaptured 全局守卫

5.2 响应式系统对比

Vue 3 内置了响应式系统,通过 refreactive 实现数据的响应式管理。状态变化会自动触发相关组件的重新渲染,无需手动订阅或监听。

示例(Vue 3):

<script setup>
import { ref } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}
</script>

<template>
  <div>
    <p>计数: {{ count }}</p>

    <button @click="increment">增加</button>

  </div>

</template>

React 通过 Hooks(如 useStateuseReducer)管理状态。状态变化会触发组件重新渲染,但没有内置的响应式系统,需要显式地管理状态更新。

示例(React):

import React, { useState } from 'react';

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

  function increment() {
    setCount(c => c + 1);
  }

  return (
    <div>
      <p>计数: {count}</p>

      <button onClick={increment}>增加</button>

    </div>

  );
}

export default Counter;

5.3 逻辑复用与组合

React HooksVue 3 Composition API 都支持逻辑复用,但实现方式略有不同。

  • React Hooks:通过自定义 Hooks 实现逻辑复用,将状态和副作用封装在独立的函数中。示例:
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchData() {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
      setLoading(false);
    }
    fetchData();
  }, [url]);

  return { data, loading };
}

export default useFetch;
  • Vue 3 Composition API:通过组合函数(Composable)实现逻辑复用,将状态和副作用封装在独立的函数中。示例:
// useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const loading = ref(true);

  onMounted(async () => {
    const response = await fetch(url);
    data.value = await response.json();
    loading.value = false;
  });

  return { data, loading };
}

6. Hooks 的最佳实践

正确使用 Hooks 能提升代码质量、可维护性和性能。以下是一些在 React Hooks 中的最佳实践。

6.1 遵循 Hooks 规则

  • 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks。确保 Hooks 在组件的顶层执行,保持 Hook 调用的顺序一致。示例:
// 正确
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// 错误
function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0); // 不要在条件语句中调用 Hooks
  }
  // ...
}
  • 只在 React 函数组件或自定义 Hooks 中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hooks。示例:
// 正确
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// 错误
function myFunction() {
  const [count, setCount] = useState(0); // 不要在普通函数中调用 Hooks
}

6.2 依赖项的正确管理

  • 确保依赖项完整:在 useEffectuseCallbackuseMemo 等 Hooks 中,依赖项数组应包含所有在副作用中使用的外部变量。
  • 避免不必要的依赖项:使用稳定的函数引用,如 useCallback 缓存回调函数,避免因函数引用变化导致副作用频繁执行。

示例:

useEffect(() => {
  fetchData(userId);
}, [userId]); // 确保包含所有依赖项

6.3 复用逻辑的方式

  • 自定义 Hooks:通过自定义 Hooks 复用状态管理逻辑,如数据获取、表单处理等。
  • 高阶组件(HOC) :使用 HOC 来复用组件逻辑,但相比自定义 Hooks,HOC 可能导致“嵌套地狱”(Wrapper Hell)。
  • 组件组合:通过组合多个组件来复用逻辑,但不如 Hooks 灵活。

推荐方式:优先选择自定义 Hooks 进行逻辑复用,保持代码的简洁和可维护性。

6.4 优化性能

  • 使用 useMemo useCallback 缓存计算结果和回调函数,避免不必要的重新创建和计算。
  • 避免在 Hooks 中定义过于复杂的逻辑,保持 Hooks 的简单性和可读性。
  • 分离状态:将状态分离到更细粒度的组件中,减少组件树中不必要的重渲染。

示例:

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

function App() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(false);

  const expensiveCalculation = useMemo(() => {
    console.log('执行昂贵的计算');
    let total = 0;
    for (let i = 0; i < 1000000; i++) {
      total += i;
    }
    return total + count;
  }, [count]);

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>计数: {count}</p>

      <button onClick={handleClick}>增加计数</button>

      <button onClick={() => setOther(!other)}>切换其他状态</button>

      <p>昂贵计算结果: {expensiveCalculation}</p>

    </div>

  );
}

export default App;

7. 常见问题与误区

在使用 React Hooks 过程中,开发者可能会遇到一些常见问题和误区。以下是一些典型的问题及其解决方案。

7.1 useEffect 中的依赖项误用

问题:

  • 遗漏依赖项:可能导致副作用未能正确响应依赖项变化。
  • 不必要的依赖项:可能导致副作用频繁执行,影响性能。

解决方案:

  • 严格遵循 ESLint 插件:使用 eslint-plugin-react-hooks 检查依赖项。
  • 使用稳定的函数引用:通过 useCallback 缓存函数,避免因函数引用变化导致副作用频繁执行。

示例:

useEffect(() => {
  doSomething(count);
}, [count]); // 确保包含所有依赖项

7.2 多个 useEffect 的执行顺序

理解:

  • React 会按照 Hooks 的调用顺序依次执行所有的 useEffect
  • 每个 useEffect 的执行顺序与其在代码中的位置一致。

示例:

function MyComponent() {
  useEffect(() => {
    console.log('Effect 1');
  }, []);

  useEffect(() => {
    console.log('Effect 2');
  }, []);

  return <div>检查控制台</div>;
}

// 控制台输出顺序:
// Effect 1
// Effect 2

7.3 useLayoutEffect 的使用场景

理解:

  • useLayoutEffect 在 DOM 更新后、浏览器绘制前同步执行,适用于需要读取布局并同步触发重渲染的场景。
  • 不建议在性能敏感的应用中过度使用 useLayoutEffect,以避免阻塞浏览器绘制。

示例:

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

function LayoutEffectExample() {
  const divRef = useRef();
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    const newHeight = divRef.current.offsetHeight;
    setHeight(newHeight);
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ height: '100px', background: 'lightblue' }}>
        Measured Div
      </div>

      <p>Div 高度: {height}px</p>

    </div>

  );
}

export default LayoutEffectExample;

8. 示例与实践

通过实际示例,可以更好地理解和应用 React Hooks。以下是一些常见的 Hooks 使用场景及其实现方式。

8.1 基本 Hooks 示例

目标:

实现一个简单的计数器,使用 useStateuseEffect 管理状态和副作用。

步骤:

  1. 创建计数器组件:
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `计数: ${count}`;
  }, [count]);

  return (
    <div>
      <p>计数: {count}</p>

      <button onClick={() => setCount(count + 1)}>增加</button>

    </div>

  );
}

export default Counter;
  1. 使用计数器组件:
import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <div>
      <h1>React Hooks 示例</h1>

      <Counter />
    </div>

  );
}

export default App;

解释:

  • useState 管理计数器的状态。
  • useEffect 在计数器更新后修改文档标题。

8.2 自定义 Hooks 示例

目标:

创建一个自定义 Hook useFetch,用于数据获取和管理加载状态。

步骤:

  1. 创建自定义 Hook:
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let isMounted = true;

    async function fetchData() {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('网络响应错误');
        }
        const result = await response.json();
        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;
  1. 使用自定义 Hook:
import React from 'react';
import useFetch from './useFetch';

function DataDisplay({ url }) {
  const { data, loading, error } = useFetch(url);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

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

export default DataDisplay;
  1. 在应用中展示数据:
import React from 'react';
import DataDisplay from './DataDisplay';

function App() {
  return (
    <div>
      <h1>数据获取示例</h1>

      <DataDisplay url="https://jsonplaceholder.typicode.com/posts/1" />
    </div>

  );
}

export default App;

解释:

  • useFetch 封装了数据获取逻辑,管理加载状态和错误状态。
  • DataDisplay 组件使用 useFetch 获取并展示数据。

8.3 复杂应用中的 Hooks 使用

目标:

在一个复杂的应用中,使用多个 Hooks 组合实现复杂的状态和副作用管理。

示例:

import React, { useState, useEffect, useReducer, useContext, createContext } from 'react';

// 创建 Context
const AppContext = createContext();

// Reducer 函数
function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_POSTS':
      return { ...state, posts: action.payload };
    default:
      throw new Error(`未知的 action 类型: ${action.type}`);
  }
}

// 自定义 Hook:管理应用状态
function useAppState() {
  const [state, dispatch] = useReducer(appReducer, { user: null, posts: [] });

  useEffect(() => {
    // 获取用户信息
    fetch('https://jsonplaceholder.typicode.com/users/1')
      .then(res => res.json())
      .then(user => dispatch({ type: 'SET_USER', payload: user }));
    
    // 获取帖子列表
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(posts => dispatch({ type: 'SET_POSTS', payload: posts }));
  }, []);

  return { state, dispatch };
}

// 应用组件
function AppProvider({ children }) {
  const appState = useAppState();
  return (
    <AppContext.Provider value={appState}>
      {children}
    </AppContext.Provider>

  );
}

// 消费 Context 的组件
function UserProfile() {
  const { state } = useContext(AppContext);
  const { user } = state;

  if (!user) return <p>加载用户信息...</p>;

  return (
    <div>
      <h2>用户信息</h2>

      <p>姓名: {user.name}</p>

      <p>邮箱: {user.email}</p>

    </div>

  );
}

function PostsList() {
  const { state } = useContext(AppContext);
  const { posts } = state;

  if (posts.length === 0) return <p>加载帖子...</p>;

  return (
    <div>
      <h2>帖子列表</h2>

      <ul>
        {posts.slice(0, 10).map(post => (
          <li key={post.id}>{post.title}</li>

        ))}
      </ul>

    </div>

  );
}

function App() {
  return (
    <AppProvider>
      <div>
        <h1>复杂应用 Hooks 示例</h1>

        <UserProfile />
        <PostsList />
      </div>

    </AppProvider>

  );
}

export default App;

解释:

  • 使用 useReducer 管理复杂的应用状态。
  • 通过 useEffect 在组件挂载时获取用户信息和帖子列表。
  • 使用 Context API 在组件树中共享状态。
  • 通过自定义 Hook useAppState 复用状态管理逻辑。

9. 总结

React Hooks 为函数组件带来了强大的状态和副作用管理能力,使得函数组件能够与类组件一样功能强大,同时代码更加简洁和可复用。通过理解和正确使用 Hooks,你可以构建高效、可维护的 React 应用。

关键要点:

  • 基础 Hooks:掌握 useStateuseEffectuseContextuseReducer 等核心 Hooks 的使用方法和适用场景。
  • 自定义 Hooks:通过自定义 Hooks 复用逻辑,提升代码的可维护性和可复用性。
  • 规则遵循:严格遵循 Hooks 的使用规则,确保代码的正确性和可预测性。
  • 性能优化:合理使用 useMemouseCallback 进行性能优化,避免不必要的重新渲染和计算。
  • 与 Vue 3 Composition API 对比:理解 React Hooks 与 Vue 3 Composition API 的异同,有助于快速适应和迁移项目。
  • 最佳实践:组织清晰的状态管理逻辑,避免常见的 Hooks 使用误区,提升应用性能和开发体验。
  • 常见问题:了解并解决常见的 Hooks 使用问题,如依赖项管理、错误处理和逻辑复用等。

通过本文的深入解析,你应该能够全面掌握 React Hooks,并将这些知识应用于实际项目中,构建出高效、可维护的 React 应用。