轻松玩转 React 三大基础 Hooks,看这一篇就够了!
何为 Hooks?——函数组件的超能力
在 React 16.8 之前,函数组件被亲切地称为"无状态组件"——它们只能接收 props 并返回 UI,无法拥有自己的状态或生命周期。而 React Hooks 的诞生彻底改变了这一格局,它赋予了函数组件类组件的所有能力,甚至更多。
Hooks 是 React 16.8 引入的一项革命性特性,它允许开发者在不编写 class 的情况下使用状态和其他 React 特性。这不是对类组件的替代,而是一种更简洁、更组合式的编程范式。
Hook 的核心哲学很简单:让函数组件也能拥有类组件的特性,但写起来更简单、逻辑更清晰。今天我们就深入探讨 React 三大基础 Hook:useState、useEffect 和 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() 的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。
所以将它作为初始化函数传递给useState,
const [todos, setTodos] = useState(createInitialTodos); 虽然结果并不会改变,但是能够优化性能,节约资源.
useEffect
useEffect 是处理副作用的 Hook,它替代了类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周方法。
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 本质上是一个生产者-消费者模式:
- 创建上下文:
createContext(defaultValue) - 提供数据:使用
Provider组件包裹需要共享数据的子树 - 消费数据:在任何后代组件中使用
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 融会贯通,构建一个主题切换器。
整体架构思路
- 状态管理:使用
useState跟踪当前主题(亮/暗) - 副作用处理:使用
useEffect将主题应用到 DOM - 跨组件通信:使用
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项目,项目结构如下:
想直接看效果的也可以使用下面的码上掘金界面:
总结与进阶思考
今天我们深入探讨了 React 三大基础 Hook:
- useState:函数组件的状态管理基石,理解其异步性和函数式更新至关重要
- useEffect:副作用的统一处理者,依赖数组是控制其行为的关键
- useContext:解决组件树深层通信的利器,但需谨慎使用以防不必要的重渲染
三者结合,我们构建了一个主题切换器,展示了如何在实际项目中应用这些概念。值得注意的是,随着应用复杂度增加,你可能会需要更复杂的状态管理方案(如 Redux、Zustand),但理解这些基础 Hook 始终是构建高质量 React 应用的前提。
正如 React 团队所说: "Hook 使代码复用变得更加容易,而不是复用状态逻辑。" 在掌握基础之后,你可以探索自定义 Hook,将相关逻辑封装成可重用的函数,进一步提升代码的可维护性和表达力。
下次当你面对状态管理、副作用处理或跨组件通信问题时,先问问自己: "这三个基础 Hook 能解决我的问题吗?" 往往,答案是肯定的。
本文基于 React 19.2.0 编写,所有代码示例均可直接运行。想要深入更多 Hook 相关内容,欢迎访问 React 官方文档。