轻松玩转 React 三大基础 Hooks,看这一篇就够了!

48 阅读8分钟

轻松玩转 React 三大基础 Hooks,看这一篇就够了!

何为 Hooks?——函数组件的超能力

在 React 16.8 之前,函数组件被亲切地称为"无状态组件"——它们只能接收 props 并返回 UI,无法拥有自己的状态或生命周期。而 React Hooks 的诞生彻底改变了这一格局,它赋予了函数组件类组件的所有能力,甚至更多。

Hooks 是 React 16.8 引入的一项革命性特性,它允许开发者在不编写 class 的情况下使用状态和其他 React 特性。这不是对类组件的替代,而是一种更简洁、更组合式的编程范式

Hook 的核心哲学很简单:让函数组件也能拥有类组件的特性,但写起来更简单、逻辑更清晰。今天我们就深入探讨 React 三大基础 Hook:useStateuseEffect 和 useContext

useState

useState 是最基础也是最常用的 Hook,它允许我们在函数组件中添加状态。让我们从最简单的定义开始:

const [state, setState] = useState(initialState);

在这个数组解构中,state 是当前状态值,setState 是更新状态的函数,initialState 是初始状态值。

1. 为组件添加状态

调用 setState 不会立即改变当前渲染中已有的状态值。这常常让初学者困惑。让我用一个简单例子说明:

function Counter() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    setCount(count + 1);
    console.log(count); // 这里打印的仍然是旧值!
  }
  
  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

当你点击按钮时,控制台打印的 count 值总是比界面上显示的小 1。这是因为 React 的状态更新是异步的 —— 调用 setCount 不会立即改变 count 的值,而是安排一次更新。当前函数执行期间,count 仍保持原值。

2. 根据先前的 state 更新 state

有时,我们需要基于当前状态计算新状态。直接使用当前状态变量可能导致闭包问题:

// 不推荐
const incrementTwice = () => {
  setCount(count + 1); // 假设 count = 0,变成 1
  setCount(count + 1); // count 仍然是 0!因为闭包捕获了旧值
};

// 推荐 - 函数式更新
const incrementTwice = () => {
  setCount(prevCount => prevCount + 1); // 使用函数获取最新状态
  setCount(prevCount => prevCount + 1); // 此时 prevCount 已经是最新的
};

让我们来看下面这个例子: 我们使用了函数式更新来实现触发一次“+3”按钮事件让age状态“+3”,请你用下面的代码替换核心部分进行调试。age只是状态,而setAge并不会更新已经运行代码中的 age 状态变量,每次执行setAge(age + 1)age的值为更新前的值, 没有 传递更新函数,所以“+3”按钮 不能按预期的方式工作

const [age, setAge] = useState(42);
  function increment() {
    setAge(age + 1);
  }

  return (
    <>
      <h1>Your age: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );

4. 避免重复创建初始状态

当初始状态计算成本较高时,可以传入函数作为 useState 的参数,避免每次渲染都重新计算:

function TodoList() {  
const [todos, setTodos] = useState(createInitialTodos()); 
// ...

如果不使用函数作为参数,而是直接调用函数的话,尽管 createInitialTodos() 的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。

所以将它作为初始化函数传递给useStateconst [todos, setTodos] = useState(createInitialTodos); 虽然结果并不会改变,但是能够优化性能,节约资源.

useEffect

useEffect 是处理副作用的 Hook,它替代了类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周方法。

useEffect(() => {
  // 副作用逻辑(数据获取、订阅、DOM操作等)
  
  return () => {
    // 清理函数(可选)
  };
}, [dependencies]); // 依赖数组

工作原理

useEffect 的行为取决于其依赖数组:

  • 无依赖数组:每次渲染后都执行
  • 空依赖数组 [] :仅在挂载和卸载时执行(类似于 componentDidMount 和 componentWillUnmount
  • 有依赖项:仅当依赖项变化时执行

理解依赖数组至关重要,它决定了 effect 何时执行,也是避免内存泄漏和竞态条件的关键。

在 Effect 中更新状态:避免无限循环

在 effect 中更新状态时需谨慎,特别是当该状态又是 effect 的依赖项时,可能造成无限循环:

// 危险!可能造成无限循环
useEffect(() => {
  setData(fetchData(count)); // 使用 count
}, [count]); // 依赖 count

// 安全方式
useEffect(() => {
  const result = fetchData(count);
  if (result !== data) {
    setData(result);
  }
}, [count, data]); // 同时依赖 count 和 data

根据先前状态更新

在 effect 中更新状态时,如果新状态依赖于旧状态,应使用函数式更新:

useEffect(() => {
  const timer = setInterval(() => {
    // 使用函数式更新确保获取最新状态
    setCount(prevCount => prevCount + 1);
  }, 1000);
  
  return () => clearInterval(timer);
}, []); // 无需将 setCount 加入依赖

useContext —— 跨层级通信的桥梁

当组件树较深时,通过 props 一层层传递数据变得繁琐而脆弱,这就是所谓的"prop-drilling"问题。useContext 提供了一种优雅的解决方案,让组件可以直接访问祖先组件提供的数据,无需中间组件介入。

原理解析:生产者-消费者模式

Context 本质上是一个生产者-消费者模式:

  1. 创建上下文createContext(defaultValue)
  2. 提供数据:使用 Provider 组件包裹需要共享数据的子树
  3. 消费数据:在任何后代组件中使用 useContext 获取数据
// 1. 创建上下文
const ThemeContext = createContext('light');

// 2. 在祖先组件提供数据
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. 在任意后代组件消费数据
function ThemeButton() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      切换到 {theme === 'light' ? '暗' : '亮'} 色主题
    </button>
  );
}

关键点:

  • Provider 的 value 属性接受任何 JavaScript 值
  • 当 value 变化时,所有消费该 Context 的组件都会重新渲染
  • Context 适合共享"全局"数据,如主题、用户认证、语言偏好等

实战案例:构建主题切换器

现在,让我们通过一个实际案例,将这三个 Hook 融会贯通,构建一个主题切换器。

整体架构思路

  1. 状态管理:使用 useState 跟踪当前主题(亮/暗)
  2. 副作用处理:使用 useEffect 将主题应用到 DOM
  3. 跨组件通信:使用 useContext 使任意组件能访问和修改主题

代码实现与分析

首先,我们创建一个主题上下文:

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

export const ThemeContext = createContext(null);

export default function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme((t) => t === 'light' ? 'dark' : 'light');
  };
  
  useEffect(() => {
    // 当主题变化时,更新根元素的 data-theme 属性
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]); // 依赖 theme,当 theme 变化时执行
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

关键点解析

  • useState 管理当前主题状态,初始值为 'light'
  • ThemeProvider 是一个包装器组件,它不渲染自己的 UI,只负责管理状态和提供数据
  • children 属性代表所有嵌套在 ThemeProvider 内部的组件
  • 通过 value={{ theme, toggleTheme }},我们把状态和修改状态的方法都提供出去

接下来,我们创建一个头部组件,用于显示和切换主题:

import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

export default function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <div style={{ marginBottom: 24 }}>
      <h2>当前主题:{theme}</h2>
      <button className="button" onClick={toggleTheme}>
        切换主题
      </button>
    </div>
  );
}

关键点解析

  • useContext(ThemeContext) 获取最近的 ThemeContext.Provider 提供的值
  • 通过解构直接获取 theme 和 toggleTheme,无需通过 props 传递
  • 按钮点击时调用 toggleTheme,触发状态更新

这个 Header 组件完全不知道 ThemeProvider 在哪、theme 状态是如何管理的,但它能使用useContext直接获取状态和修改方法!这就是上下文的力量。

最后,设置 CSS 变量来支持主题切换:

/* 定义基础变量 */
:root {
  --bg-color: #ffffff;
  --text-color: #222;
  --primary-color: #1677ff;
}

/* 暗色主题覆盖变量 */
[data-theme='dark'] {
  --bg-color: #141414;
  --text-color: #f5f5f5;
  --primary-color: #4e8cff;
}

/* 应用变量到 body */
body {
  margin: 0;
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: all 0.3s;
}

/* 按钮样式 */
.button {
  padding: 8px 16px;
  background: var(--primary-color);
  color: #fff;
  border: none;
  cursor: pointer;
}

关键点解析

  • 使用 CSS 变量(Custom Properties)定义主题相关样式
  • 通过属性选择器 [data-theme='dark'] 覆盖暗色主题变量
  • 应用 transition 使主题切换有平滑动画
  • 全局样式通过变量实现主题一致性

应用入口将所有部分组合起来:

import Page from './pages/Page';
import ThemeProvider from './contexts/ThemeContext';

export default function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  );
}

在vscode中使用vite迅速搭建react项目,项目结构如下:

image.png

想直接看效果的也可以使用下面的码上掘金界面:

总结与进阶思考

今天我们深入探讨了 React 三大基础 Hook:

  1. useState:函数组件的状态管理基石,理解其异步性和函数式更新至关重要
  2. useEffect:副作用的统一处理者,依赖数组是控制其行为的关键
  3. useContext:解决组件树深层通信的利器,但需谨慎使用以防不必要的重渲染

三者结合,我们构建了一个主题切换器,展示了如何在实际项目中应用这些概念。值得注意的是,随着应用复杂度增加,你可能会需要更复杂的状态管理方案(如 Redux、Zustand),但理解这些基础 Hook 始终是构建高质量 React 应用的前提。

正如 React 团队所说: "Hook 使代码复用变得更加容易,而不是复用状态逻辑。" 在掌握基础之后,你可以探索自定义 Hook,将相关逻辑封装成可重用的函数,进一步提升代码的可维护性和表达力。

下次当你面对状态管理、副作用处理或跨组件通信问题时,先问问自己: "这三个基础 Hook 能解决我的问题吗?" 往往,答案是肯定的。


本文基于 React 19.2.0 编写,所有代码示例均可直接运行。想要深入更多 Hook 相关内容,欢迎访问 React 官方文档