React Hooks 深入浅出

目录

  1. [引言:React Hooks 的革命](#引言:React Hooks 的革命)
  2. [基础 Hooks](#基础 Hooks)
  3. [额外的 Hooks](#额外的 Hooks)
    • useReducer:复杂状态逻辑的管理
    • [useCallback 与 useMemo:性能优化利器](#useCallback 与 useMemo:性能优化利器)
    • [useRef:引用 DOM 和保存变量](#useRef:引用 DOM 和保存变量)
  4. [自定义 Hooks:逻辑复用的最佳实践](#自定义 Hooks:逻辑复用的最佳实践)
  5. [Hooks 使用规则与陷阱](#Hooks 使用规则与陷阱)
  6. [实战案例:使用 Hooks 重构传统组件](#实战案例:使用 Hooks 重构传统组件)
  7. 总结与展望

引言:React Hooks 的革命

React Hooks 是 React 16.8 版本中引入的特性,它彻底改变了 React 组件的编写方式。在 Hooks 出现之前,我们需要使用类组件来管理状态和生命周期,而函数组件则被视为"无状态组件"。Hooks 的出现使得函数组件也能够拥有状态和生命周期功能,从而简化了组件逻辑,提高了代码的可读性和可维护性。

Hooks 解决了 React 中的一些长期存在的问题:

  • 组件之间难以复用状态逻辑:在 Hooks 之前,我们通常使用高阶组件(HOC)或 render props 模式来复用组件逻辑,但这些方法往往导致组件嵌套过深,形成"嵌套地狱"。
  • 复杂组件变得难以理解:生命周期方法中常常混杂着不相关的逻辑,而相关逻辑却分散在不同的生命周期方法中。
  • 类组件的困惑 :类组件需要理解 JavaScript 中 this 的工作方式,这对新手不太友好。

接下来,我们将深入探讨 React Hooks 的各个方面,从基础用法到高级技巧,帮助你全面掌握这一强大特性。

基础 Hooks

useState:状态管理的新方式

useState 是最基础也是最常用的 Hook,它让函数组件能够拥有自己的状态。

jsx 复制代码
import React, { useState } from 'react';

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

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

useState 返回一个数组,包含两个元素:当前状态值和一个更新该状态的函数。我们使用数组解构来获取这两个值。

useState 的高级用法

  1. 函数式更新:当新的状态依赖于之前的状态时,推荐使用函数式更新。
jsx 复制代码
// 不推荐
setCount(count + 1);

// 推荐
setCount(prevCount => prevCount + 1);
  1. 惰性初始化 :如果初始状态需要通过复杂计算获得,可以传递一个函数给 useState
jsx 复制代码
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useEffect:组件生命周期的替代方案

useEffect 让你在函数组件中执行副作用操作,如数据获取、订阅或手动更改 DOM 等。它统一了 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期方法。

jsx 复制代码
import React, { useState, useEffect } from 'react';

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

  // 类似于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `你点击了 ${count} 次`;
    
    // 返回一个清理函数,类似于 componentWillUnmount
    return () => {
      document.title = 'React App';
    };
  }, [count]); // 仅在 count 更改时更新

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

useEffect 的依赖数组

  • 空数组 [] :效果只在组件挂载和卸载时执行一次,类似于 componentDidMountcomponentWillUnmount
  • 有依赖项 [a, b]:效果在组件挂载时以及依赖项变化时执行。
  • 无依赖数组:效果在每次渲染后执行。

useContext:简化 Context API

useContext 让你可以订阅 React 的 Context,而不必使用 Context.Consumer 组件。

jsx 复制代码
import React, { useContext } from 'react';

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

function ThemedButton() {
  // 使用 useContext 获取当前主题
  const theme = useContext(ThemeContext);
  
  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>
      我是一个主题按钮
    </button>
  );
}

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

useContext 接收一个 Context 对象(由 React.createContext 创建)并返回该 Context 的当前值。当 Provider 更新时,使用该 Context 的组件会重新渲染。

额外的 Hooks

useReducer:复杂状态逻辑的管理

useReduceruseState 的替代方案,适用于有复杂状态逻辑的场景,特别是当下一个状态依赖于之前的状态时。

jsx 复制代码
import React, { useReducer } from 'react';

// 定义 reducer 函数
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() {
  // 使用 useReducer
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

useReducer 返回当前状态和 dispatch 函数。dispatch 函数用于触发状态更新,它接收一个 action 对象,通常包含 type 属性和可选的 payload。

useCallback 与 useMemo:性能优化利器

这两个 Hooks 主要用于性能优化,避免不必要的计算和渲染。

useCallback:返回一个记忆化的回调函数,只有当依赖项变化时才会更新。

jsx 复制代码
import React, { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 使用 useCallback 记忆化回调函数
  const handleClick = useCallback(() => {
    console.log(`Button clicked, count: ${count}`);
  }, [count]); // 只有当 count 变化时,handleClick 才会更新
  
  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// 使用 React.memo 优化子组件
const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

useMemo:返回一个记忆化的值,只有当依赖项变化时才重新计算。

jsx 复制代码
import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ a, b }) {
  // 使用 useMemo 记忆化计算结果
  const result = useMemo(() => {
    console.log('Computing result...');
    // 假设这是一个耗时的计算
    return a * b;
  }, [a, b]); // 只有当 a 或 b 变化时,才重新计算
  
  return <div>Result: {result}</div>;
}

useRef:引用 DOM 和保存变量

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。

引用 DOM 元素

jsx 复制代码
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  // 创建一个 ref
  const inputRef = useRef(null);
  
  // 点击按钮时聚焦输入框
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

保存变量

jsx 复制代码
import React, { useState, useRef, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);
  // 使用 useRef 保存 interval ID
  const intervalRef = useRef(null);
  
  useEffect(() => {
    // 设置定时器
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    // 清理定时器
    return () => {
      clearInterval(intervalRef.current);
    };
  }, []); // 空依赖数组,只在挂载和卸载时执行
  
  // 停止计时器
  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

useState 不同,useRef.current 属性变化不会触发组件重新渲染。

自定义 Hooks:逻辑复用的最佳实践

自定义 Hooks 是 React Hooks 最强大的特性之一,它允许你将组件逻辑提取到可重用的函数中。自定义 Hook 是一个以 "use" 开头的 JavaScript 函数,可以调用其他 Hooks。

示例:创建一个 useLocalStorage Hook

jsx 复制代码
import { useState, useEffect } from 'react';

// 自定义 Hook:使用 localStorage 持久化状态
function useLocalStorage(key, initialValue) {
  // 初始化状态
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // 尝试从 localStorage 获取值
      const item = window.localStorage.getItem(key);
      // 如果存在则解析并返回,否则返回初始值
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });
  
  // 更新 localStorage 的函数
  const setValue = value => {
    try {
      // 允许值是一个函数,类似于 useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      // 保存到 state
      setStoredValue(valueToStore);
      // 保存到 localStorage
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };
  
  return [storedValue, setValue];
}

// 使用自定义 Hook
function App() {
  const [name, setName] = useLocalStorage('name', 'Bob');
  
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
      />
    </div>
  );
}

示例:创建一个 useWindowSize Hook

jsx 复制代码
import { useState, useEffect } from 'react';

// 自定义 Hook:获取窗口尺寸
function useWindowSize() {
  // 初始化状态
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });
  
  useEffect(() => {
    // 处理窗口大小变化的函数
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    
    // 添加事件监听器
    window.addEventListener('resize', handleResize);
    
    // 初始调用一次以设置初始值
    handleResize();
    
    // 清理函数
    return () => window.removeEventListener('resize', handleResize);
  }, []); // 空依赖数组,只在挂载和卸载时执行
  
  return windowSize;
}

// 使用自定义 Hook
function ResponsiveComponent() {
  const size = useWindowSize();
  
  return (
    <div>
      {size.width < 768 ? (
        <p>在小屏幕上显示</p>
      ) : (
        <p>在大屏幕上显示</p>
      )}
    </div>
  );
}

Hooks 使用规则与陷阱

使用 Hooks 时,必须遵循两条重要规则:

  1. 只在最顶层使用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks,确保 Hooks 在每次组件渲染时都以相同的顺序被调用。
jsx 复制代码
// ❌ 错误:在条件语句中使用 Hook
function Form() {
  const [name, setName] = useState('Mary');
  
  if (name !== '') {
    useEffect(() => {
      localStorage.setItem('name', name);
    });
  }
  
  // ...
}

// ✅ 正确:将条件放在 Hook 内部
function Form() {
  const [name, setName] = useState('Mary');
  
  useEffect(() => {
    if (name !== '') {
      localStorage.setItem('name', name);
    }
  });
  
  // ...
}
  1. 只在 React 函数组件或自定义 Hooks 中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hooks。

常见陷阱与解决方案

  1. 依赖数组遗漏
jsx 复制代码
// ❌ 错误:依赖数组遗漏了 count
function Counter({ count }) {
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, []); // 依赖数组为空,但使用了 count
  
  // ...
}

// ✅ 正确:添加所有依赖项
function Counter({ count }) {
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]); // 正确添加了 count 作为依赖项
  
  // ...
}
  1. 闭包陷阱
jsx 复制代码
// ❌ 问题:使用过时的状态值
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(`Current count: ${count}`);
      setCount(count + 1); // 这里的 count 是闭包捕获的初始值
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 依赖数组为空,导致 count 始终为初始值 0
  
  // ...
}

// ✅ 解决方案:使用函数式更新
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1); // 使用函数式更新获取最新状态
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 现在可以安全地使用空依赖数组
  
  // ...
}
  1. 过度依赖 useEffect
jsx 复制代码
// ❌ 不必要的 useEffect
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  
  // 不必要的 useEffect,可以直接计算
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);
  
  // ...
}

// ✅ 更好的方式:直接计算派生状态
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  
  // 直接计算派生值,无需额外的状态和 useEffect
  const fullName = `${firstName} ${lastName}`;
  
  // ...
}

实战案例:使用 Hooks 重构传统组件

让我们通过一个实际例子,展示如何将类组件重构为使用 Hooks 的函数组件。

原始类组件

jsx 复制代码
import React, { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      loading: true,
      error: null
    };
  }

  componentDidMount() {
    this.fetchUserData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUserData();
    }
  }

  fetchUserData = async () => {
    this.setState({ loading: true });
    try {
      const response = await fetch(`https://api.example.com/users/${this.props.userId}`);
      const data = await response.json();
      this.setState({ user: data, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  };

  render() {
    const { user, loading, error } = this.state;

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!user) return <div>No user data</div>;

    return (
      <div>
        <h1>{user.name}</h1>
        <p>Email: {user.email}</p>
        <p>Phone: {user.phone}</p>
      </div>
    );
  }
}

使用 Hooks 重构后的函数组件

jsx 复制代码
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUserData() {
      setLoading(true);
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }

    fetchUserData();
  }, [userId]); // 依赖项数组,只有当 userId 变化时才重新获取数据

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>No user data</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

进一步优化:提取自定义 Hook

jsx 复制代码
import React, { useState, useEffect } from 'react';

// 自定义 Hook:获取用户数据
function useUserData(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUserData() {
      setLoading(true);
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }

    fetchUserData();
  }, [userId]);

  return { user, loading, error };
}

// 使用自定义 Hook 的组件
function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>No user data</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

通过这个例子,我们可以看到 Hooks 带来的几个明显优势:

  1. 代码更简洁:函数组件比类组件代码量少,更易于阅读和理解。
  2. 关注点分离:通过自定义 Hook,我们可以将数据获取逻辑与 UI 渲染逻辑分离。
  3. 逻辑复用:自定义 Hook 可以在多个组件之间复用,而不需要使用 HOC 或 render props。

总结与展望

React Hooks 彻底改变了 React 组件的编写方式,使函数组件成为了 React 开发的主流。通过本文,我们深入探讨了 React Hooks 的基础知识、高级用法、常见陷阱以及最佳实践。

Hooks 的优势总结:

  1. 简化组件逻辑:使用 Hooks,我们可以将相关的逻辑放在一起,而不是分散在不同的生命周期方法中。
  2. 促进逻辑复用:自定义 Hooks 提供了一种比 HOC 和 render props 更简洁的逻辑复用方式。
  3. 更好的类型推断:在 TypeScript 中,Hooks 比类组件有更好的类型推断。
  4. 减少代码量:函数组件通常比等效的类组件代码量少。
  5. 更容易测试:纯函数更容易测试,Hooks 使得编写纯函数组件变得更加容易。

随着 React 的发展,Hooks 生态系统也在不断壮大。React 团队还在探索更多的 Hooks,如 useTransitionuseDeferredValue,以解决并发模式下的性能问题。同时,社区也开发了大量的第三方 Hooks 库,如 react-useuse-http 等,进一步扩展了 Hooks 的能力。


参考资料:

  1. React 官方文档 - Hooks 介绍
  2. React Hooks 完全指南
  3. 使用 React Hooks 的常见错误
  4. 深入理解 React useEffect
相关推荐
随笔记17 分钟前
react-router里的两种路由方式有什么不同
前端·react.js
前端李二牛17 分钟前
异步任务并发控制
前端·javascript
你也向往长安城吗38 分钟前
推荐一个三维导航库:three-pathfinding-3d
javascript·算法
karrigan1 小时前
async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 执行引擎的精细管理
javascript
wycode1 小时前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode2 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏2 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
我是哈哈hh3 小时前
【Node.js】ECMAScript标准 以及 npm安装
开发语言·前端·javascript·node.js
张元清3 小时前
电商 Feeds 流缓存策略:Temu vs 拼多多的技术选择
前端·javascript·面试
晴空雨3 小时前
React 合成事件原理:从事件委托到 React 17 的重大改进
前端·react.js