React Hooks 自 2018 年发布以来,彻底改变了我们编写 React 组件的方式。它们允许我们在不编写 class 的情况下使用 state 以及其他的 React 特性,让代码更加简洁、可复用且易于理解。本文将深入探讨几个最核心和常用的 Hooks,包括 React 内置的useState、useEffect、useMemo,以及强大的数据获取和缓存库 TanStack Query (@tanstack/react-query) 提供的useQuery、useMutation、useQueryClient,还有用于路由导航的useNavigate。通过理论与实例相结合,帮助您全面掌握它们的使用场景和最佳实践。
一、React 内置 Hooks 核心详解
1.1 useState:管理组件的本地状态
useState是构建交互式 UI 的基石。它让你在函数组件中拥有并更新本地状态。
核心概念:
useState(initialValue)接收一个初始状态值。- 返回一个数组
[state, setState]。state:当前的状态值。setState:一个用于更新状态的函数。
示例:
import{useState}from'react';functionCounter(){const[count,setCount]=useState(0);// 初始值为 0constincrement=()=>{// 状态更新函数可以接收一个函数,以确保基于最新的状态值进行更新setCount(prevCount=>prevCount+1);};return(<div><p>Count:{count}</p><button onClick={increment}>+</button></div>);}1.2 useEffect:处理副作用与生命周期
useEffect让你可以执行那些可能产生“副作用”的操作,例如数据获取、订阅或手动更改 DOM。它巧妙地整合了组件挂载、更新和卸载的逻辑。
核心概念:
useEffect(didUpdate, dependencies?)didUpdate:包含副作用逻辑的函数。这个函数可以选择性地返回一个清理函数。dependencies?:依赖数组。useEffect的行为取决于这个数组。[](空数组):副作用仅在组件挂载时执行一次,相当于componentDidMount。[dep1, dep2]:副作用在组件挂载,以及dep1或dep2发生变化时执行。undefined(无):副作用在每次渲染后都会执行,相当于componentDidMount和componentDidUpdate的组合。
示例:数据获取
import{useState,useEffect}from'react';functionUserProfile({userId}){const[user,setUser]=useState(null);const[loading,setLoading]=useState(true);useEffect(()=>{letisCancelled=false;// 用于防止在组件卸载后设置状态asyncfunctionfetchUserData(){setLoading(true);try{constresponse=awaitfetch(`/api/users/${userId}`);constuserData=awaitresponse.json();if(!isCancelled){setUser(userData);}}catch(error){if(!isCancelled){console.error("Fetch error:",error);}}finally{if(!isCancelled){setLoading(false);}}}if(userId){fetchUserData();}// 清理函数,在组件卸载或下次 effect 执行前运行return()=>{isCancelled=true;};},[userId]);// 依赖 userId,当 userId 变化时重新执行if(loading)return<p>Loading...</p>;if(!user)return<p>No user found.</p>;return<h1>{user.name}</h1>;}1.3 useMemo:优化昂贵的计算
useMemo是性能优化的利器。它可以“记住”一个计算的结果,只有当它的依赖项发生改变时,才会重新计算。
核心概念:
useMemo(calculateValue, dependencies)calculateValue:一个返回所需值的函数。dependencies:一个依赖项数组。calculateValue只有在任何一个依赖项改变时才会被重新执行。
示例:缓存复杂计算结果
import{useState,useMemo}from'react';functionExpensiveList({items,filterTerm}){const[count,setCount]=useState(0);// 只有 items 或 filterTerm 改变时,才会重新过滤列表constfilteredItems=useMemo(()=>{console.log("Re-running expensive calculation...");// 用于演示returnitems.filter(item=>item.name.includes(filterTerm));},[items,filterTerm]);return(<div><p>Count:{count}<button onClick={()=>setCount(c=>c+1)}>+</button></p><ul>{filteredItems.map(item=><li key={item.id}>{item.name}</li>)}</ul></div>);}二、TanStack Query:服务器状态管理的专家
在现代 Web 应用中,与服务器的数据交互非常频繁。手动管理加载、错误、缓存等逻辑既繁琐又容易出错。TanStack Query (原 React Query) 为此而生,它提供了强大的工具来简化服务器状态的管理。
2.1 useQuery:优雅地获取数据
useQuery是获取和管理服务器数据的首选 Hook。它内置了缓存、后台同步、请求去重、错误处理等强大功能。
核心概念:
useQuery({ queryKey, queryFn, ...options })queryKey:一个唯一标识查询的数组,例如['user', userId]。它是缓存和失效的关键。queryFn:一个返回 Promise 的异步函数,用于执行实际的 API 请求。options:其他配置选项,如enabled(是否启用查询)、staleTime(数据被认为是陈旧的时间)、cacheTime(数据在缓存中保留的时间) 等。
示例:
import{useQuery}from'@tanstack/react-query';functionUserDetail({userId}){const{data:user,isLoading,isError,error,refetch,// 提供一个手动刷新数据的函数}=useQuery({queryKey:['user',userId],// 查询键queryFn:async()=>{constresponse=awaitfetch(`/api/users/${userId}`);if(!response.ok){thrownewError('Network response was not ok');}returnresponse.json();},enabled:!!userId,// 仅当 userId 存在时才发起请求});if(isLoading)return<div>Loading...</div>;if(isError)return<div>Error:{error.message}</div>;return(<div><h2>{user.name}</h2><p>{user.email}</p><button onClick={()=>refetch()}>Refetch</button></div>);}2.2 useMutation:处理数据变更
useMutation用于处理创建、更新、删除等修改服务器数据的操作。它专注于处理这些“写”操作的生命周期。
核心概念:
useMutation({ mutationFn, onSuccess, onError, ...options })mutationFn:一个执行写操作的异步函数。onSuccess(data, variables, context):操作成功时的回调。onError(error, variables, context):操作失败时的回调。mutate(variables):触发 mutation 的函数,需要传入mutationFn所需的参数。
示例:
import{useMutation,useQueryClient}from'@tanstack/react-query';functionCreateTodoForm(){constqueryClient=useQueryClient();constmutation=useMutation({mutationFn:async(newTodo)=>{constresponse=awaitfetch('/api/todos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(newTodo),});if(!response.ok){thrownewError('Failed to create todo');}returnresponse.json();},onSuccess:(newTodo)=>{// 成功后,可以手动更新缓存或使其失效// 例如,使所有 todos 的查询失效,让它们在下次渲染时重新获取queryClient.invalidateQueries({queryKey:['todos']});// 或者更精确地更新特定查询的缓存// queryClient.setQueryData(['todos'], (oldTodos) => [...oldTodos, newTodo]);},onError:(error)=>{console.error('Create error:',error);}});consthandleSubmit=(e)=>{e.preventDefault();constform=e.target;consttitle=form.title.value.trim();if(title){mutation.mutate({title,completed:false});form.reset();// 重置表单}};return(<form onSubmit={handleSubmit}><input name="title"placeholder="What needs to be done?"/><button type="submit"disabled={mutation.isPending}>{mutation.isPending?'Saving...':'Add Todo'}</button>{mutation.isError&&<span style={{color:'red'}}>Error:{mutation.error.message}</span>}</form>);}2.3 useQueryClient:访问查询客户端
useQueryClientHook 允许你在组件内部访问 TanStack Query 的QueryClient实例。这让你可以手动控制缓存,比如预取数据、更新缓存或使其失效。
核心概念:
const queryClient = useQueryClient();- 通过
queryClient对象,你可以调用其方法,如queryClient.invalidateQueries()、queryClient.setQueryData()、queryClient.prefetchQuery()等。
在useMutation中的应用:
上面useMutation的示例已经展示了useQueryClient的一个重要用法:在数据变更成功后,使相关的缓存失效,以确保 UI 显示最新的数据。
三、Hooks 之间的关系与区别:为什么我们需要这么多?
许多开发者可能会疑惑,既然useQuery和useMutation已经能很好地处理数据的 CRUD (创建、读取、更新、删除),并且useQuery本身就带有缓存功能,那为什么还需要useEffect和useMemo呢?它们之间有何不同?
3.1 useEffect vs. useQuery/useMutation:职责划分
useQuery和useMutation的核心职责是管理服务器状态 (Server State)。它们专注于与后端 API 的交互,包括获取、修改、缓存和同步数据。
useEffect的职责则更广泛,它处理所有副作用 (Side Effects)。副作用是指那些不在 React 渲染过程中发生,但会影响组件或外部世界的行为。useQuery和useMutation本身也是基于useEffect构建的,但它们将其封装成了针对特定场景的专用工具。
useEffect的典型应用场景包括:
- 订阅外部事件:例如,监听浏览器窗口大小变化 (
window.addEventListener('resize', handler))、键盘事件、或者 WebSocket 消息。 - 手动操作 DOM:在组件挂载后,获取 DOM 元素并进行聚焦 (
focus())、滚动 (scrollIntoView)、或执行自定义动画。 - 启动和清理计时器:使用
setInterval或setTimeout,并在组件卸载时通过清理函数清除它们,防止内存泄漏。 - 初始化第三方库:在组件挂载时初始化像 Chart.js 图表、Mapbox 地图等需要 DOM 元素才能工作的库。
- 执行非数据获取类的副作用:例如,在某个状态变化后发送 Google Analytics 事件。
总结:useQuery/useMutation专门处理服务器数据,而useEffect处理所有其他类型的副作用。
3.2 useMemo vs. useQuery 缓存:缓存的不同层面
useQuery的缓存和useMemo的缓存虽然都叫“缓存”,但它们解决的问题和作用的层面完全不同。
useQuery的缓存:- 目的:缓存服务器返回的数据 (Server Data)。
- 好处:减少不必要的网络请求,提高应用响应速度,避免重复加载相同的数据。
- 范围:通常存储在 TanStack Query 的全局缓存实例中,可以在多个组件间共享。
- 触发:由
queryKey和网络请求决定。
useMemo的缓存:- 目的:缓存组件渲染期间的计算结果 (Computed Values)。
- 好处:避免在每次组件重新渲染时都执行昂贵的计算,提升渲染性能。
- 范围:仅存在于组件的内存中,与组件实例绑定。
- 触发:由
useMemo的依赖数组 (depsarray) 决定。
举例说明:
假设你用useQuery获取了一个包含大量用户的数组users。现在你想根据用户输入的searchTerm来过滤这个列表。
// 不使用 useMemofunctionUserList({searchTerm}){const{data:users=[],isLoading}=useQuery({queryKey:['users'],queryFn:fetchUsers});// 每次 searchTerm 改变导致组件重渲染时,这个过滤操作都会被执行!// 如果 users 很大,这会很耗性能。constfilteredUsers=users.filter(user=>user.name.toLowerCase().includes(searchTerm.toLowerCase()));if(isLoading)return<div>Loading...</div>;return<ul>{filteredUsers.map(user=><li key={user.id}>{user.name}</li>)}</ul>;}// 使用 useMemofunctionUserList({searchTerm}){const{data:users=[],isLoading}=useQuery({queryKey:['users'],queryFn:fetchUsers});// 只有当 users 数组 或 searchTerm 改变时,才会重新执行过滤操作constfilteredUsers=useMemo(()=>{console.log("Filtering users...");// 仅在必要时打印,证明计算被跳过returnusers.filter(user=>user.name.toLowerCase().includes(searchTerm.toLowerCase()));},[users,searchTerm]);// 注意依赖项if(isLoading)return<div>Loading...</div>;return<ul>{filteredUsers.map(user=><li key={user.id}>{user.name}</li>)}</ul>;}在这个例子中:
useQuery缓存了从服务器获取的原始users数据。useMemo缓存了对users数据进行过滤计算后的结果filteredUsers。
useQuery保证了users不会被重复请求,而useMemo保证了users没变时,过滤计算不会重复执行。两者配合使用,能同时优化网络请求和渲染性能。
总结:useQuery缓存服务器数据,useMemo缓存组件内的计算结果。
四、React Router DOM:编程式导航
当应用变得复杂,路由逻辑不再仅仅依赖于用户点击链接时,我们就需要编程式导航。
4.1 useNavigate:掌控导航方向
useNavigateHook 提供了一个navigate函数,让你可以在代码的任何地方执行导航操作。
核心概念:
const navigate = useNavigate();navigate(to, options?):执行导航。to:目标路径(字符串)或偏移量(数字,如-1表示后退)。options?:可选配置,如{ replace: boolean }(替换历史记录栈顶)、{ state: any }(传递状态)。
示例:登录后的重定向
import{useState}from'react';import{useNavigate}from'react-router-dom';functionLoginPage(){const[username,setUsername]=useState('');const[password,setPassword]=useState('');const[error,setError]=useState('');constnavigate=useNavigate();// 获取 navigate 函数consthandleSubmit=async(e)=>{e.preventDefault();setError('');try{constresponse=awaitfetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,password}),});if(response.ok){// 登录成功,导航到仪表盘navigate('/dashboard',{replace:true});// 使用 replace 防止用户点后退按钮回到登录页}else{consterrorData=awaitresponse.json();setError(errorData.message||'Login failed');}}catch(err){setError('Network error');}};return(<form onSubmit={handleSubmit}><input value={username}onChange={(e)=>setUsername(e.target.value)}placeholder="Username"/><input type="password"value={password}onChange={(e)=>setPassword(e.target.value)}placeholder="Password"/><button type="submit">Log In</button>{error&&<p style={{color:'red'}}>{error}</p>}</form>);}结语
本文系统地介绍了useState,useEffect,useMemo这三个 React 核心 Hooks,以及useQuery,useMutation,useQueryClient这套强大的 TanStack Query 工具集,还有useNavigate这个路由导航利器。我们还深入探讨了useEffect和useMemo与useQuery/useMutation的区别,明确了它们各自独特的职责和应用场景。理解并熟练运用这些 Hooks,是构建现代化、高性能 React 应用的关键。希望这篇指南能为您提供清晰、实用的参考。