React Hooks基础:从useState到useContext核心API详解

可以这么理解,React Hooks 就是 2018 年 React 16.8 引入的革命性特性,它让咱们能在函数组件里直接用状态和其他 React 特性,再也不用写那些绕来绕去的 class 组件了。现在最新的稳定版是 React 18.2.0(2022年6月发布的),这个版本里的 Hooks 已经非常成熟稳定,咱们平时开发基本都是基于这个版本。

useState:最基础的状态管理

useState 应该是咱们用得最多的 Hook 了,它用来声明组件的局部状态。很多新手刚接触的时候容易实战经验,比如直接修改状态值。关键点:永远不要直接修改 state,一定要用 setState 函数

看个最基础的计数器例子:

import { useState } from 'react'; function Counter() { // 声明一个叫 count 的状态,初始值是 0 // setCount 是用来更新 count 的函数 const [count, setCount] = useState(0); return ( <div style={{ padding: '20px' }}> <h2>当前计数:{count}</h2> <button onClick={() => setCount(count + 1)} style={{ marginRight: '10px', padding: '8px 16px' }} > 加 1 </button> <button onClick={() => setCount(prev => prev - 1)} style={{ padding: '8px 16px' }} > 减 1 </button> <button onClick={() => setCount(0)} style={{ marginLeft: '10px', padding: '8px 16px' }} > 重置 </button> </div> ); } export default Counter;

这里有个细节要注意,setCount 可以传一个新值,也可以传一个函数,这个函数会接收之前的状态值作为参数。当你新的状态依赖之前的状态时,比如这里的减 1,用函数形式更稳妥,因为 React 的状态更新可能是异步的,直接用 count - 1 有时候拿到的不是最新值。

useReducer:复杂状态逻辑救星

当状态逻辑比较复杂,比如有多个字段,或者下一个状态依赖之前多个状态的时候,useReducer 就派上用场了。换个角度看,它和 useState 类似,但更适合处理复杂的状态更新逻辑,尤其是像表单处理这种场景。

咱们用它实现一个简单的多字段表单:

import { useReducer } from 'react'; // 定义 reducer 函数,接收当前状态和 action,返回新状态 function formReducer(state, action) { switch (action.type) { case 'UPDATE_FIELD': return { ...state, [action.field]: action.value }; case 'RESET': return { username: '', email: '', password: '' }; default: return state; } } function RegisterForm() { // 初始状态 const initialState = { username: '', email: '', password: '' }; const [formData, dispatch] = useReducer(formReducer, initialState); const handleChange = (e) => { const { name, value } = e.target; dispatch({ type: 'UPDATE_FIELD', field: name, value: value }); }; const handleSubmit = (e) => { e.preventDefault(); console.log('表单数据:', formData); dispatch({ type: 'RESET' }); }; return ( <form onSubmit={handleSubmit} style={{ padding: '20px', maxWidth: '400px' }}> <h2>注册表单</h2> <div style={{ marginBottom: '15px' }}> <label>用户名:</label> <input type="text" name="username" value={formData.username} onChange={handleChange} style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <div style={{ marginBottom: '15px' }}> <label>邮箱:</label> <input type="email" name="email" value={formData.email} onChange={handleChange} style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <div style={{ marginBottom: '15px' }}> <label>密码:</label> <input type="password" name="password" value={formData.password} onChange={handleChange} style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <button type="submit" style={{ padding: '10px 20px' }}>提交</button> </form> ); } export default RegisterForm;

这里 useReducer 接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。通过 dispatch 不同的 action 来更新状态,逻辑非常清晰,比用多个 useState 管理表单字段要舒服多了。

useEffect:副作用处理专家

组件里除了渲染 UI,经常需要做些其他事情,比如数据请求、订阅事件、操作 DOM 这些,这些就是副作用。useEffect 就是专门处理这些的。

它的第二个参数是依赖数组,这个特别重要,很多新手在这里实际案例:

看个数据请求的例子:

import { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // 定义一个异步函数 const fetchUsers = async () => { try { setLoading(true); const response = await fetch('https://jsonplaceholder.typicode.com/users'); if (!response.ok) { throw new Error('请求失败'); } const data = await response.json(); setUsers(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUsers(); // 清理函数,组件卸载时执行,比如取消订阅、清除定时器 return () => { console.log('组件卸载,清理副作用'); }; }, []); // 空依赖数组,只执行一次 if (loading) return <div style={{ padding: '20px' }}>加载中...</div>; if (error) return <div style={{ padding: '20px', color: 'red' }}>错误:{error}</div>; return ( <div style={{ padding: '20px' }}> <h2>用户列表</h2> <ul> {users.map(user => ( <li key={user.id} style={{ marginBottom: '10px' }}> {user.name} - {user.email} </li> ))} </ul> </div> ); } export default UserList;

值得留意的是,useEffect 里的清理函数很重要,比如你设置了定时器,一定要在清理函数里清除,不然组件卸载了定时器还在跑,那就是内存泄漏了。

useContext:跨组件状态共享

平时我们传值都是 props 一层层往下传,要是组件层级深了,那就很麻烦,这就是所谓的“props 钻透”。useContext 就是解决这个问题,让我们可以跨组件共享状态,不用一层层传 props。

先创建一个 Context:

import { createContext, useContext, useState } from 'react'; // 创建 Context const ThemeContext = createContext(); // 主题提供者组件 function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } // 使用主题的组件 function ThemeButton() { // 用 useContext 获取 Context 里的值 const { theme, toggleTheme } = useContext(ThemeContext); return ( <button onClick={toggleTheme} style={{ padding: '10px 20px', backgroundColor: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#333' : '#fff', border: '1px solid #ccc', borderRadius: '4px' }} > 当前主题:{theme},点击切换 </button> ); } // 根组件 function App() { return ( <ThemeProvider> <div style={{ padding: '20px' }}> <h2>Context 示例</h2> <ThemeButton /> </div> </ThemeProvider> ); } export default App;

这里 ThemeProvider 里的组件都能通过 useContext 获取主题状态,不管层级多深,不用一层层传 props。不过要注意,useContext 会导致组件在 Context 值变化时重新渲染,所以如果 Context 里的值变化频繁,最好拆分成多个 Context。

💡 经验总结

useState 和 useReducer 怎么选? 简单状态用 useState,比如单个值、简单的对象;复杂状态逻辑,比如多个字段联动、状态更新逻辑复杂,就用 useReducer,维护起来更清晰。如果是 React 18.2.0 及以后版本,还可以结合 useMemo 优化 reducer 相关的计算,后面性能优化章节会详细讲。

React 19新特性实战:use()与useEffectEvent尝鲜指南

现在 React 19 Beta 已经在 2024年4月发布了,里面有不少让人眼前一亮的新 Hooks 特性,虽然还是 Beta 版,但咱们可以提前尝尝鲜,看看未来 React 的发展方向。可以这么理解,这些新特性就是为了让咱们写代码更简单,少写点模板代码。

use() hook:直接读取 Promise 和 Context

React 19 里最火的新 Hook 就是 use() 了,它能让咱们在组件里直接读取 Promise 或者 Context 的值。之前咱们读 Context 要用 useContext,请求数据要在 useEffect 里处理,现在用 use() 可能更方便。不过社区里现在有个争议,就是 use() 可以在条件判断里用,打破了之前 Hooks“只能在顶层调用”的规则,这个咱们后面会聊。

先看看用 use() 读取 Context 的例子,对比下之前的 useContext

import { createContext, use } from 'react'; const UserContext = createContext(null); function UserProfile() { // 用 use() 读取 Context,不用再 import useContext 了 // 注意:现在还是 React 19 Beta,所以要从 'react' 里导入 use const user = use(UserContext); if (!user) { // 这里可以在条件里用 use(),之前 Hooks 是不允许的 // 不过这也是争议点,会不会让代码逻辑变乱? return <div style={{ padding: '20px' }}>请先登录</div>; } return ( <div style={{ padding: '20px' }}> <h2>用户信息</h2> <p>用户名:{user.name}</p> <p>邮箱:{user.email}</p> </div> ); } function App() { const user = { name: '张三', email: 'zhangsan@example.com' }; return ( <UserContext.Provider value={user}> <UserProfile /> </UserContext.Provider> ); } export default App;

再看看用 use() 处理 Promise 的例子,这个更实用,之前咱们请求数据要在 useEffect 里写一堆 try/catch、loading、error 状态,现在用 use() 配合 Suspense 就能简化:

import { use, Suspense } from 'react'; // 模拟一个返回 Promise 的数据请求 function fetchUser() { return new Promise((resolve) => { setTimeout(() => { resolve({ id: 1, name: '李四', email: 'lisi@example.com' }); }, 1500); }); } function UserDetail() { // 直接调用 use() 读取 Promise,React 会暂停渲染直到 Promise 完成 // 这个过程会被最近的 Suspense 捕获 const user = use(fetchUser()); return ( <div style={{ padding: '20px' }}> <h2>用户详情</h2> <p>ID:{user.id}</p> <p>姓名:{user.name}</p> <p>邮箱:{user.email}</p> </div> ); } function App() { return ( <div style={{ padding: '20px' }}> <h2>use() 处理 Promise 示例</h2> {/* Suspense 会捕获 use() 等待 Promise 的过程,显示 fallback */} <Suspense fallback={<div>加载用户信息中...</div>}> <UserDetail /> </Suspense> </div> ); } export default App;

简单来说,use() 读取 Promise 的时候,组件会暂停渲染,直到 Promise resolve,这个暂停会被最近的 Suspense 组件捕获,显示 fallback 内容。这样就不用咱们手动管理 loading 状态了,代码简洁很多。不过要注意,use() 读取的 Promise 最好是稳定的,不然每次渲染都创建新的 Promise,会一直触发重新请求。

useEffectEvent:分离副作用中的事件逻辑

之前我们写 useEffect 的时候,经常会在副作用里用到一些函数,这些函数如果依赖外部状态,就得加到依赖数组里,不然会有过期闭包的问题。但有时候这些函数其实就是个事件处理逻辑,和渲染无关,加依赖数组里挺麻烦的。React 19 里的 useEffectEvent 就是解决这个问题的,它能把副作用里的事件逻辑分离出来,这些事件函数不用加到 useEffect 的依赖数组里。

看个例子,比如我们想在窗口 resize 的时候打印当前的计数:

import { useState, useEffect, useEffectEvent } from 'react'; function ResizeLogger() { const [count, setCount] = useState(0); const [windowWidth, setWindowWidth] = useState(window.innerWidth); // 用 useEffectEvent 定义一个事件函数,不用加到 useEffect 的依赖里 const onResize = useEffectEvent(() => { // 这里可以直接访问最新的 count,不用把 count 加到依赖数组 console.log('窗口大小变化,当前计数:', count); setWindowWidth(window.innerWidth); }); useEffect(() => { const handleResize = () => { onResize(); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []); // 依赖数组为空,因为 onResize 是 useEffectEvent 定义的,不需要作为依赖 return ( <div style={{ padding: '20px' }}> <h2>窗口宽度:{windowWidth}px</h2> <p>计数:{count}</p> <button onClick={() => setCount(c => c + 1)} style={{ padding: '8px 16px' }} > 增加计数 </button> </div> ); } export default ResizeLogger;

值得留意的是,useEffectEvent 定义的函数,在 useEffect 里调用的时候,不用加到依赖数组里,而且它能访问到最新的 props 和 state,不会有过期闭包的问题。这个特性如果稳定下来,能解决很多 useEffect 依赖数组的痛点。不过现在还是 React 19 Beta 的特性,生产环境暂时别用,等稳定版出来再考虑。

新特性带来的思考与争议

现在社区里对 use() hook 争议挺大的,之前 React 官方一直强调 Hooks 必须在组件顶层调用,不能在条件、循环里用,现在 use() 打破了这个规则,很多人担心会让代码逻辑变混乱,不好维护。比如你在条件里用 use(),条件不满足的时候就不执行,那组件的行为就不好预测了。

还有服务端组件(RSC)和 Hooks 的融合,React 19 里服务端组件可以先用 use() 获取数据,然后把数据传给客户端组件,客户端组件再用 useState 这些 Hooks 处理交互,这样能减少客户端的 bundle 体积,这个趋势挺明显的。比如一个博客文章页面,服务端组件用 use() 获取文章内容,然后传给客户端的评论组件,评论组件用 useState 管理评论输入,这样文章内容不用打包到客户端里。

📌 要点提醒

现在要不要用 React 19 Beta 的新特性? 个人建议,学习可以,但生产项目暂时别用,毕竟还是 Beta 版,API 可能会变。如果实在想尝鲜,可以建个小 demo 试试,看看 use()useEffectEvent 怎么用,了解未来的发展方向。等 React 19 稳定版出来,这些特性落地了,再考虑在项目里用。另外,TypeScript 对 React 19 新 Hooks 的类型推断也在完善,现在用可能会遇到类型报错的问题,这个要注意。

自定义Hooks设计模式:封装useFetch与useLocalStorage

写了这么多基础 Hooks,咱们肯定不想每次都重复写那些逻辑,比如每次请求数据都要写 useState 管理 loading、error、data,每次用 localStorage 都要写读取、写入、监听变化的逻辑。这时候自定义 Hooks 就派上用场了,打个比方,把可复用的逻辑封装成一个函数,名字以 use 开头,里面可以调用其他 Hooks,这样在任何组件里都能直接用。

自定义Hooks的基本规则

首先得记住,自定义 Hooks 本质上就是个 JavaScript 函数,但有两个规则:

封装useLocalStorage:持久化状态

咱们先封装一个 useLocalStorage,作用就是像 useState 一样用状态,但这个状态会自动同步到 localStorage,页面刷新也不会丢。这个在存用户偏好设置、登录 token 这些场景特别有用。

import { useState, useEffect } from 'react'; // 自定义 Hook:useLocalStorage function useLocalStorage(key, initialValue) { // 从 localStorage 读取初始值,如果有的话就用,没有就用 initialValue const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); // 如果 item 存在,就解析 JSON,否则返回初始值 return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(`读取 localStorage 键 ${key} 失败:`, error); return initialValue; } }); // 当 storedValue 变化时,同步到 localStorage useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error(`写入 localStorage 键 ${key} 失败:`, error); } }, [key, storedValue]); // 监听其他标签页对 localStorage 的修改,同步状态 useEffect(() => { const handleStorageChange = (e) => { if (e.key === key) { // 如果其他标签页修改了这个 key,就更新状态 try { setStoredValue(e.newValue ? JSON.parse(e.newValue) : initialValue); } catch (error) { console.error(`解析 localStorage 变化失败:`, error); } } }; window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, [key, initialValue]); return [storedValue, setStoredValue]; } // 使用 useLocalStorage 的组件 function ThemeSetting() { // 用 useLocalStorage 管理主题,初始值是 'light' const [theme, setTheme] = useLocalStorage('app-theme', 'light'); const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; return ( <div style={{ padding: '20px', backgroundColor: theme === 'light' ? '#f5f5f5' : '#333', color: theme === 'light' ? '#333' : '#f5f5f5', minHeight: '100px' }}> <h2>主题设置</h2> <p>当前主题:{theme}</p> <button onClick={toggleTheme} style={{ padding: '8px 16px', backgroundColor: theme === 'light' ? '#fff' : '#666', color: theme === 'light' ? '#333' : '#fff', border: '1px solid #ccc' }} > 切换主题 </button> <p>刷新页面后主题会保留</p> </div> ); } export default ThemeSetting;

这个 useLocalStorage 做了几件事:初始化的时候从 localStorage 读值,状态变化的时候同步到 localStorage,还监听了 storage 事件,这样其他标签页修改了同一个 key,当前组件也能同步更新。核心要点:读取 localStorage 的时候要做 try/catch,因为有些浏览器禁用了 localStorage,或者存储的值不是合法的 JSON,会报错。

封装useFetch:简化数据请求

接下来封装一个 useFetch,把请求数据时的 loading、error、data 状态都封装进去,以后任何组件要请求数据,直接调用这个 Hook 就行,不用重复写那些逻辑。如果是在 React 18.2.0 项目里,还可以结合第三方 Hooks 比如 useSWR 或者 useQuery,但咱们先自己实现一个基础的版本,理解原理。

import { useState, useEffect } from 'react'; // 自定义 Hook:useFetch function useFetch(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // 如果 url 不存在,就不请求 if (!url) { setLoading(false); return; } const fetchData = async () => { try { setLoading(true); setError(null); const response = await fetch(url, options); if (!response.ok) { throw new Error(`请求失败:${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err.message || '请求出错'); } finally { setLoading(false); } }; fetchData(); // 清理函数,如果组件卸载了,就不更新状态 return () => { // 这里可以用 AbortController 取消请求,更严谨 const controller = new AbortController(); options.signal = controller.signal; return () => controller.abort(); }; }, [url, JSON.stringify(options)]); // 依赖 url 和 options,options 变化了就重新请求 return { data, loading, error }; } // 使用 useFetch 的组件 function PostList() { // 调用 useFetch 请求文章列表 const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5'); if (loading) return <div style={{ padding: '20px' }}>加载文章中...</div>; if (error) return <div style={{ padding: '20px', color: 'red' }}>错误:{error}</div>; return ( <div style={{ padding: '20px' }}> <h2>文章列表</h2> <ul> {posts?.map(post => ( <li key={post.id} style={{ marginBottom: '15px', padding: '10px', border: '1px solid #eee' }}> <h3>{post.title}</h3> <p>{post.body}</p> </li> ))} </ul> </div> ); } export default PostList;

这里 useFetch 接收 url 和 options 作为参数,返回 data、loading、error 三个状态。依赖数组里用了 JSON.stringify(options),因为 options 是个对象,直接放依赖数组里,每次渲染对象引用变化,都会触发重新请求,所以序列化一下比较稳妥。当然,更好的做法是让调用 useFetch 的地方用 useMemo 包裹 options,避免不必要的重新请求。

自定义Hooks的最佳实践

现在社区里经常讨论“Hooks 地狱”的问题,就是有人把逻辑拆得太细,一个组件里调用十几个自定义 Hooks,看起来很乱。注意,自定义 Hooks 要适度拆分,一个 Hook 只做一件事,但也不要拆得太碎。比如 useFetch 就只做数据请求相关的逻辑,不用把数据处理的逻辑也加进去,数据处理可以让调用它的组件自己做,或者再封装一个专门处理数据的 Hook。

还有,自定义 Hooks 里也可以调用其他自定义 Hooks,比如我们可以封装一个 useUser,里面调用 useFetchuseLocalStorage

function useUser() { const { data: user, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users/1'); const [token, setToken] = useLocalStorage('user-token', ''); return { user, loading, error, token, setToken, isLoggedIn: !!token && !error }; }

这样在组件里直接用 useUser 就能拿到用户信息和登录状态,非常方便。

📌 要点提醒

自定义 Hooks 要不要处理所有边界情况? 不用,看场景。如果是自己项目用的,处理常见的边界情况就行,比如 useLocalStorage 处理 JSON 解析失败、useFetch 处理网络错误。如果是要开源给别人用的,就得处理更多边界情况,比如 useFetch 支持取消请求、支持不同的响应类型(不是只支持 JSON)、支持请求重试这些。另外,自定义 Hooks 最好加上 TypeScript 类型,比如 useLocalStorage 可以加泛型,指定存储的值的类型,这样用起来更友好,React 19 之后 TypeScript 对 Hooks 的类型推断也会更精准,这个可以期待下。

性能优化进阶:useMemo、useCallback与useTransition避坑

写 React 组件的时候,经常会遇到性能问题,比如父组件状态变了,子组件明明没用到这个状态,也跟着重新渲染了;或者一个计算很耗时的函数,每次渲染都执行,导致页面卡顿。这时候就需要性能优化相关的 Hooks 了:useMemo 缓存计算结果、useCallback 缓存函数、useTransition 标记非紧急更新。换个角度看,这些 Hooks 就是帮我们减少不必要的计算和渲染,让页面更流畅。

useMemo:缓存计算结果

useMemo 的作用是缓存一个计算的结果,只有当依赖的值变化的时候,才会重新计算,否则直接返回之前缓存的结果。它接收两个参数:一个是计算函数,一个是依赖数组,和 useEffect 类似。

看个例子,比如我们有一个耗时的计算,比如过滤一个很长的列表:

import { useState, useMemo } from 'react'; // 模拟一个很长的列表 const bigList = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `项目 ${i}`, category: i % 3 === 0 ? 'A' : i % 3 === 1 ? 'B' : 'C' })); function FilteredList() { const [filter, setFilter] = useState('all'); const [count, setCount] = useState(0); // 不用 useMemo 的话,每次 count 变化,这里都会重新过滤列表,即使 filter 没变 // const filteredList = bigList.filter(item => { // console.log('过滤列表'); // 每次渲染都会打印 // return filter === 'all' || item.category === filter; // }); // 用 useMemo 缓存过滤结果,只有 filter 变化的时候才重新过滤 const filteredList = useMemo(() => { console.log('过滤列表'); return bigList.filter(item => { return filter === 'all' || item.category === filter; }); }, [filter]); // 依赖数组只有 filter,filter 不变就不会重新计算 return ( <div style={{ padding: '20px' }}> <h2>过滤列表</h2> <div style={{ marginBottom: '15px' }}> <button onClick={() => setFilter('all')} style={{ marginRight: '10px', padding: '8px 16px', backgroundColor: filter === 'all' ? '#007bff' : '#fff', color: filter === 'all' ? '#fff' : '#333' }} > 全部 </button> <button onClick={() => setFilter('A')} style={{ marginRight: '10px', padding: '8px 16px', backgroundColor: filter === 'A' ? '#007bff' : '#fff', color: filter === 'A' ? '#fff' : '#333' }} > 分类 A </button> <button onClick={() => setFilter('B')} style={{ marginRight: '10px', padding: '8px 16px', backgroundColor: filter === 'B' ? '#007bff' : '#fff', color: filter === 'B' ? '#fff' : '#333' }} > 分类 B </button> <button onClick={() => setFilter('C')} style={{ padding: '8px 16px', backgroundColor: filter === 'C' ? '#007bff' : '#fff', color: filter === 'C' ? '#fff' : '#333' }} > 分类 C </button> </div> <p>过滤后的数量:{filteredList.length}</p> <button onClick={() => setCount(c => c + 1)} style={{ marginBottom: '15px', padding: '8px 16px' }} > 增加计数(触发重新渲染):{count} </button> <ul style={{ maxHeight: '300px', overflowY: 'auto' }}> {filteredList.map(item => ( <li key={item.id} style={{ padding: '5px', borderBottom: '1px solid #eee' }}> {item.name} - {item.category} </li> ))} </ul> </div> ); } export default FilteredList;

这里如果没有 useMemo,每次点击“增加计数”按钮,组件重新渲染,过滤列表的逻辑都会执行一遍,即使 filter 没变,控制台也会打印“过滤列表”。用了 useMemo 之后,只有 filter 变化的时候才会重新过滤,点击计数按钮的时候不会,性能提升明显。核心要点:useMemo 不是必须的,只有当计算很耗时,或者依赖的组件重新渲染频繁的时候才用,不然反而会增加内存开销,因为要缓存结果。

useCallback:缓存函数

useCallbackuseMemo 类似,不过它缓存的是函数,而不是计算结果。为什么需要缓存函数呢?因为函数组件每次渲染都会创建一个新的函数,如果你把这个新函数作为 props 传给子组件,子组件用 React.memo 包裹的话,每次都会认为 props 变了,触发重新渲染。这时候用 useCallback 缓存函数,只要依赖不变,函数引用就不变,子组件就不会重新渲染。

看个例子,父组件传一个函数给子组件:

import { useState, useCallback, memo } from 'react'; // 用 React.memo 包裹子组件,只有 props 变化的时候才重新渲染 const ChildButton = memo(function ChildButton({ onClick, label }) { console.log('子组件渲染'); return ( <button onClick={onClick} style={{ padding: '8px 16px', margin: '5px' }}> {label} </button> ); }); function ParentComponent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); // 不用 useCallback 的话,每次父组件渲染,handleClick 都是新函数,子组件会重新渲染 // const handleClick = () => { // console.log('按钮点击'); // }; // 用 useCallback 缓存函数,依赖数组为空,函数引用永远不变 const handleClick = useCallback(() => { console.log('按钮点击'); }, []); // 如果这个函数用到了 count 等状态,就要加到依赖数组里 // 这个函数依赖 text,只有 text 变化的时候才会创建新函数 const handleTextClick = useCallback(() => { console.log('当前文本:', text); }, [text]); return ( <div style={{ padding: '20px' }}> <h2>useCallback 示例</h2> <p>计数:{count}</p> <button onClick={() => setCount(c => c + 1)} style={{ padding: '8px 16px', marginRight: '10px' }} > 增加计数 </button> <input value={text} onChange={e => setText(e.target.value)} placeholder="输入文本" style={{ padding: '8px', marginRight: '10px' }} /> <div style={{ marginTop: '15px' }}> <ChildButton onClick={handleClick} label="点击我(不依赖状态)" /> <ChildButton onClick={handleTextClick} label="点击我(依赖文本状态)" /> </div> </div> ); } export default ParentComponent;

这里 ChildButtonmemo 包裹了,所以如果传的 onClick 函数引用不变,就不会重新渲染。如果不用 useCallback,每次父组件渲染,handleClick 都是新的函数,子组件每次都会渲染,控制台会打印“子组件渲染”。用了 useCallback 之后,点击“增加计数”按钮,父组件渲染,但 handleClick 引用不变,第一个子组件不会渲染;而 handleTextClick 依赖 text,输入文本的时候 text 变化,handleTextClick 才会创建新函数,第二个子组件才会渲染。

useTransition:标记非紧急更新

useTransition 是 React 18 引入的新 Hook,用来标记一些非紧急的状态更新,让这些更新不会阻塞紧急的更新(比如用户输入、点击)。其实,就是把更新分成两类:紧急的(用户交互)和非紧急的(比如列表过滤、搜索结果更新),非紧急的更新可以稍后再做,这样页面就不会卡顿。

它返回一个数组,第一个值是一个布尔值 isPending,表示非紧急更新是否正在进行;第二个值是一个函数 startTransition,用来包裹非紧急的更新。

看个例子,比如一个搜索框,输入的时候过滤一个大列表:

import { useState, useTransition, useMemo } from 'react'; // 模拟一个大列表 const allItems = Array.from({ length: 20000 }, (_, i) => `项目 ${ ## 5. Hooks原理与规则:为什么不能在条件语句中调用? 很多刚接触React Hooks的同学,第一次看到官方文档那个**“不要在循环、条件或嵌套函数中调用 Hook”**的规则时,心里大概率会犯嘀咕:“我代码逻辑就是需要判断一下再执行啊,凭啥不让我写?” 打个比方,这其实不是React团队故意刁难开发者,而是由Hooks底层的**链表实现机制**决定的。咱们得先搞懂React是怎么“记住”你声明的这些Hooks的。 React在内部维护了一个**Hooks链表**,你可以把它想象成一个有序的清单。当函数组件首次渲染(挂载)时,React会按照你写代码的顺序,依次执行`useState`、`useEffect`等。比如你写了两个`useState`,React就会在链表里生成两个节点,第一个节点存第一个状态,第二个节点存第二个状态。 **关键点来了**:React并不靠名字来区分这些Hooks(不像Class组件靠`this.state`的key),它靠的是**调用顺序**。下次组件更新时,React会再次按顺序执行这些Hooks,然后对照链表里的节点去取对应的状态。如果你把Hook写在了条件语句里,比如这样:

function Counter() {

if (Math.random() > 0.5) {

const [count, setCount] = useState(0); // 危险操作!

}

const [name, setName] = useState('张三');

return

{name}
;

}

假设第一次渲染时随机数大于0.5,链表里记录了`count`和`name`两个状态。第二次渲染时随机数小于0.5,`count`的`useState`没执行,这时候React还是会按顺序去取:它以为第一个节点是`count`,结果取到了第二个节点里的`name`的值,这就乱套了!状态错位,组件直接崩掉或者表现诡异。 咱们来看一个**错误示范**和**正确示范**的对比。 ### 错误示范:条件语句中调用Hook

import React, { useState } from 'react';

function UserProfile({ isAdmin }) {

// ❌ 错误:在条件语句中调用 useState

if (isAdmin) {

const [adminLevel, setAdminLevel] = useState(1);

}

const [userName, setUserName] = useState('Guest');

return (

用户: {userName}

{/* 这里逻辑会因为状态错位而报错 */}

);

}

### 正确示范:将条件移到Hook内部

import React, { useState } from 'react';

function UserProfile({ isAdmin }) {

// ✅ 正确:Hook在顶层调用

const [adminLevel, setAdminLevel] = useState(1);

const [userName, setUserName] = useState('Guest');

return (

用户: {userName}

{isAdmin && (

管理员等级: {adminLevel}

)}

);

}

React Hooks的设计基于一个假设,就是**Hook的调用顺序在每次渲染中都是一样的**。一旦顺序变了,React就懵了。 其实除了不能在条件里写,也不能在循环、普通函数里写。Hooks只能直接在**React函数组件**或者**自定义Hooks**的顶层调用。 ### 📌 要点提醒 如果你用的是VS Code,强烈建议安装 **ESLint** 配合 `eslint-plugin-react-hooks` 插件。它能自动帮你检测这种违规写法,在你写代码的时候直接报错标红,比你自己盯着看靠谱多了。配置也很简单,在`.eslintrc`里加上:

{

"plugins": ["react-hooks"],

"rules": {

"react-hooks/rules-of-hooks": "error",

"react-hooks/exhaustive-deps": "warn"

}

}

这样你再想写条件Hook,编辑器立马给你报警告,直接把坑扼杀在摇篮里。目前最新的**React 18.2.0**依然严格遵守这个规则,哪怕是**React 19 Beta**,虽然引入了`use()`这样的新特性,但这个基础规则依然没变,这是Hooks的基石。 --- ## 6. 2024趋势展望:React Compiler如何改变Hooks开发模式 咱们写React的时候,最烦的一件事大概就是**性能优化**了。尤其是那个`useMemo`和`useCallback`,简直是“又爱又恨”。爱它是因为它能救命,防止子组件瞎渲染;恨它是因为它写起来太麻烦,依赖数组还得手动维护,稍微漏写一个依赖,代码就出Bug。 不过,风向要变了。2024年React社区最炸裂的话题,绝对是**React Compiler(React编译器)**。这玩意儿要是全面落地,咱们写Hooks的姿势可能会发生翻天覆地的变化。 其实,React Compiler就是一个**自动优化工具**。以前的React,你得手动告诉它:“嘿,这段代码很贵,帮我缓存一下(用`useMemo`)”或者“这个函数别老变,帮我记住(用`useCallback`)”。现在,React Compiler来了,它会在代码构建阶段自动分析你的代码,然后像变魔术一样,自动帮你加上这些优化。 咱们看个对比。以前我们要写一个带缓存的计算函数,得这么写: ### 手动优化时代(现在的主流写法)

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

function ExpensiveList({ items, filterText }) {

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

// 必须手动使用 useMemo,还得小心写对依赖数组 [items, filterText]

const filteredItems = useMemo(() => {

console.log('过滤数据...');

return items.filter(item => item.name.includes(filterText));

}, [items, filterText]);

return (

    {filteredItems.map(item => (

  • {item.name}
  • ))}

);

}

这段代码没啥问题,但是如果你忘了写`[items, filterText]`,或者多写了个`count`进去,都可能出事儿。而且代码看着也不清爽。 ### React Compiler 时代(未来的写法) 如果React Compiler成熟了,你只需要写纯粹的逻辑,编译器会自动帮你处理缓存:

// 假设 React Compiler 已经启用

import React, { useState } from 'react';

function ExpensiveList({ items, filterText }) {

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

// 不需要写 useMemo!编译器会自动识别这里的逻辑是否昂贵,并决定是否注入缓存

const filteredItems = items.filter(item => item.name.includes(filterText));

return (

    {filteredItems.map(item => (

  • {item.name}
  • ))}

);

}

看到没?代码瞬间干净了。你不需要再纠结要不要包一层`useMemo`,编译器比你更懂哪里该优化。 **React 19 Beta** 已经在为这种趋势铺路了。除了编译器,React 19还引入了`use()` Hook。这东西挺有意思,它允许你在组件里直接读取Promise或者Context。以前我们用`useEffect`去请求数据,现在可能直接用`use()`配合Suspense就行。不过社区里也在吵,说`use()`打破了Hooks“只能在顶层调用”的规则,因为它可以在条件里用(虽然目前还是实验阶段)。 ### 服务端组件(RSC)与Hooks的融合 还有个趋势是**服务端组件(RSC)**。以前Hooks主要是客户端的事儿,现在RSC来了,很多数据获取的逻辑可以直接在服务端完成。这时候,`useState`这种客户端Hook依然在客户端跑,而数据获取可能就交给服务端的`async/await`或者`use()`了。这种协同模式,能大大减少咱们打包到客户端的JS体积。 ### 📖 学习建议 虽然React Compiler很香,但**现在(2024年中)还别急着在生产环境全量上**。目前它还在积极开发中,Meta内部虽然已经在用了,但对外的稳定版还没完全普及。 不过,你现在可以做的是:**养成良好的代码习惯**。不要滥用`useMemo`和`useCallback`。记住,只有当你发现性能真的有问题,或者依赖数组确实复杂到难以维护时,再去用它们。如果React Compiler将来真的普及了,那些被你滥用的`useMemo`反而可能成为累赘。 另外,关注一下**TypeScript**的类型推断。随着React 19的推进,TS对Hooks的类型支持会更智能,比如`useState`的初始值类型推导会更准,这也能帮你少写点类型定义的代码。咱们做全栈的,就得时刻盯着这些能提升效率的新玩意儿。 --- ## 7. 总结与常见面试问题:Hooks核心知识点速查 咱们这篇长文写到现在,把Hooks从基础用法到原理,再到未来趋势都过了一遍。最后这部分,咱们来点干货,把这玩意儿彻底嚼碎了咽下去。特别是准备面试的同学,这一章就是你的“急救包”。 咱们先快速回顾一下核心Hooks,然后直接上**面试考点梳理**。 ### 核心Hooks速查表 - **`useState`**:最基础的状态管理。适合简单的、独立的变量。 - **`useEffect`**:处理副作用。记住它的三个档位:空数组(挂载时)、有值(监听变化)、不写(每次都跑)。 - **`useMemo`**:缓存计算结果,防止每次渲染都重新算一遍。 - **`useCallback`**:缓存函数,防止函数引用变化导致子组件重渲染。 - **`useRef`**:存东西,变了不触发渲染,常用来拿DOM或者存定时器ID。 - **`useContext`**:跨组件传值,不用一层层往下扒拉props。 ### 常见面试问题解析 **1. `useState` 和 `useReducer` 有啥区别?啥时候用哪个?** 其实,`useState`就是个简单的“存值器”,适合单个值或者简单的对象。比如一个开关的`true/false`,或者一个输入框的内容。 而`useReducer`更像是一个“状态机”,适合逻辑复杂、有多个子值,或者下一个状态依赖于之前状态的情况。 **场景举例**:如果你做一个表单,只有用户名和密码,用`useState`两个变量就搞定了。但如果你做一个购物车,里面有添加、删除、修改数量、清空购物车,这一堆逻辑用`useReducer`写起来会非常清晰,用`useState`就会写一堆`setXXX`,乱得不行。 **2. `useEffect` 的依赖数组,空数组、有值、不写,到底啥区别?** 这是必考题,千万别搞混。 - **不写数组**:`useEffect(fn)`。组件每次渲染(包括第一次)都会执行。这玩意儿千万别乱用,容易死循环。 - **空数组**:`useEffect(fn, [])`。只在组件第一次挂载时执行一次。相当于Class组件里的`componentDidMount`。常用来发起初始化请求。 - **有值**:`useEffect(fn, [count, name])`。第一次挂载会执行,之后只要`count`或者`name`这两个变量变了,它就会再次执行。 **3. `useMemo` 和 `useCallback` 是不是必须要用?** **不是!** 这是个巨大的坑。很多新手为了“性能优化”,给每个函数、每个变量都包一层,结果代码又臭又长。 注意,`useMemo`和`useCallback`本身也是消耗性能的(它们要存值、要比对依赖)。只有当你的组件**渲染频率很高**,或者传递给子组件的**函数/对象引用经常变**导致子组件跟着瞎渲染时,才需要用它们。普通场景下,别瞎用。 **4. 为什么Hooks不能在条件语句里调用?** 这个问题咱们在第5章详细讲过。核心就是**调用顺序**。React靠链表顺序来对应状态,条件语句会打乱顺序,导致状态错位。回答的时候提一下“链表”这个词,面试官眼睛会亮一下。 ### 实战代码示例:综合应用 咱们写个稍微综合点的例子,把`useReducer`和`useContext`结合起来,模拟一个简单的全局状态管理(比如用户登录态)。

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

// 1. 定义 Context

const AuthContext = createContext(null);

// 2. 定义 Reducer

const authReducer = (state, action) => {

switch (action.type) {

case 'LOGIN':

return { ...state, user: action.payload, isLoggedIn: true };

case 'LOGOUT':

return { ...state, user: null, isLoggedIn: false };

default:

return state;

}

};

// 3. 定义 Provider 组件

export const AuthProvider = ({ children }) => {

const [state, dispatch] = useReducer(authReducer, { user: null, isLoggedIn: false });

// 用 useCallback 缓存 action,防止不必要的渲染

const login = useCallback((userData) => {

dispatch({ type: 'LOGIN', payload: userData });

}, []);

const logout = useCallback(() => {

dispatch({ type: 'LOGOUT' });

}, []);

return (

{children}

);

};

// 4. 自定义 Hook 方便使用

export const useAuth = () => {

const context = useContext(AuthContext);

if (!context) {

throw new Error('useAuth 必须在 AuthProvider 内使用');

}

return context;

};

// 5. 使用组件

const UserStatus = () => {

const { state, login, logout } = useAuth();

if (!state.isLoggedIn) {

return ;

}

return (

欢迎, {state.user.name}

);

};

// 在 App 里包裹

//

### 📖 学习建议 面试的时候,如果问到Hooks的未来,你可以提一嘴**React 19**和**React Compiler**。比如你可以说:“虽然现在Hooks还需要手动优化,但随着React Compiler的发展,未来这部分工作可能会被自动化,开发体验会更好。” 这能体现出你不仅懂现在,还关注技术趋势。 另外,社区里现在也在讨论`useEffect`是不是被过度使用了,甚至有人提议用`useSyncExternalStore`来替代一些副作用场景。平时写代码时,多思考一下是不是非得用`useEffect`,有时候把逻辑移到事件处理函数里可能更合适。这就是资深工程师和初级工程师的区别——初级的啥都往`useEffect`里塞,资深的知道什么时候该用,什么时候不该用。