React Hooks 与导航
掌握 React Hooks 在 Next.js 中的应用及页面导航的最佳实践。
📚 学习目标
学完这篇文章后,你将能够:
- 掌握 Next.js 内置 Hooks 的使用
- 学会自定义 Hooks 封装可复用逻辑
- 掌握编程式导航的方法
- 了解本项目的 Hooks 使用模式
- 学会状态管理最佳实践
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- React Hooks 基础(useState、useEffect)
- 路由基础概念
1️⃣ Next.js 内置 Hooks
1.1 useRouter - 编程式导航
useRouter 用于在 Client Components 中进行编程式导航。
基本用法:
'use client';
import { useRouter } from 'next/navigation';
export default function NavigationExample() {
const router = useRouter();
const handleClick = () => {
// 导航到首页
router.push('/');
// 替换当前页面(不添加历史记录)
router.replace('/login');
// 返回上一页
router.back();
// 前进
router.forward();
// 刷新当前页面
router.refresh();
};
return <button onClick={handleClick}>导航</button>;
}查看本项目的使用:app/login/page.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useAuth } from '@/app/contexts/AuthContext';
export default function LoginPage() {
const router = useRouter();
const { user } = useAuth();
// 如果已登录,跳转到首页
useEffect(() => {
if (user) {
router.push('/');
}
}, [user, router]);
// ... 登录逻辑
}1.2 usePathname - 获取当前路径
usePathname 获取当前 URL 的路径部分。
基本用法:
'use client';
import { usePathname } from 'next/navigation';
export default function CurrentPath() {
const pathname = usePathname();
return <div>当前路径: {pathname}</div>;
}查看本项目的使用:app/components/ProtectedRoute.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useAuth } from '@/app/contexts/AuthContext';
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const pathname = usePathname();
// 公开路由
const publicRoutes = ['/login', '/register'];
if (loading) return <div>加载中...</div>;
if (!user && !publicRoutes.includes(pathname)) {
// 未登录,跳转到登录页
return <div>请先登录</div>;
}
return <>{children}</>;
}1.3 useSearchParams - 获取查询参数
useSearchParams 获取 URL 的查询参数。
基本用法:
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get('q'); // ?q=hello
const page = searchParams.get('page'); // ?page=1
const sort = searchParams.get('sort'); // ?sort=date
return (
<div>
<p>搜索词: {query}</p>
<p>页码: {page}</p>
<p>排序: {sort}</p>
</div>
);
}设置查询参数:
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
export default function SortControl() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const setSort = (sort: string) => {
const current = new URLSearchParams(Array.from(searchParams.entries()));
current.set('sort', sort);
router.push(`${pathname}?${current.toString()}`);
};
return (
<div>
<button onClick={() => setSort('date')}>按日期排序</button>
<button onClick={() => setSort('name')}>按名称排序</button>
</div>
);
}2️⃣ 自定义 Hooks
2.1 为什么需要自定义 Hooks?
自定义 Hooks 用于封装可复用的逻辑,避免代码重复。
优势:
- 📦 逻辑复用
- 🧪 易于测试
- 🔄 状态管理集中
- 📖 代码更清晰
2.2 基本语法
'use client';
import { useState, useEffect } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 使用
export default function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>重置</button>
</div>
);
}2.3 本项目的自定义 Hooks
useSessionManager - 会话管理
查看实现:app/hooks/useSessionManager.ts
'use client';
import { useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import type { Session } from '@/app/database/sessions';
export function useSessionManager() {
const [sessions, setSessions] = useState<Session[]>([]);
const [currentSession, setCurrentSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(false);
const router = useRouter();
const pathname = usePathname();
// 加载会话列表
const loadSessions = async () => {
setLoading(true);
try {
const response = await fetch('/api/chat/sessions');
const data = await response.json();
setSessions(data.sessions);
// 如果有当前会话 ID,加载它
const currentSessionId = searchParams.get('session');
if (currentSessionId) {
const session = data.sessions.find(
(s: Session) => s.id === currentSessionId,
);
if (session) setCurrentSession(session);
}
} catch (e) {
console.error('加载会话失败:', e);
} finally {
setLoading(false);
}
};
// 创建会话
const createSession = async (name: string, type: 'chat' | 'deepresearch') => {
const response = await fetch('/api/chat/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type }),
});
const data = await response.json();
await loadSessions();
return data;
};
// 删除会话
const deleteSession = async (id: string) => {
const response = await fetch('/api/chat/sessions', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
await loadSessions();
};
// 重命名会话
const renameSession = async (id: string, name: string) => {
const response = await fetch('/api/chat/sessions', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, name }),
});
await loadSessions();
};
// 切换会话
const switchSession = (session: Session) => {
setCurrentSession(session);
router.push(
`/${session.type === 'deepresearch' ? 'deepresearch' : ''}?session=${session.id}`,
);
};
useEffect(() => {
loadSessions();
}, []);
return {
sessions,
currentSession,
loading,
loadSessions,
createSession,
deleteSession,
renameSession,
switchSession,
};
}useChatMessages - 消息管理
查看实现:app/hooks/useChatMessages.ts
'use client';
import { useState } from 'react';
import type { Message } from '@/app/database/messages';
export function useChatMessages() {
const [messages, setMessages] = useState<Message[]>([]);
// 添加消息
const addMessage = (message: Message) => {
setMessages((prev) => [...prev, message]);
};
// 更新消息
const updateMessage = (messageId: string, updates: Partial<Message>) => {
setMessages((prev) =>
prev.map((msg) => (msg.id === messageId ? { ...msg, ...updates } : msg)),
);
};
// 删除消息
const deleteMessage = (messageId: string) => {
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
};
// 清空消息
const clearMessages = () => {
setMessages([]);
};
return {
messages,
addMessage,
updateMessage,
deleteMessage,
clearMessages,
};
}3️⃣ 导航方式对比
3.1 Link 组件 vs useRouter
| 特性 | Link 组件 | useRouter |
|---|---|---|
| 使用场景 | 静态导航链接 | 动态编程导航 |
| 声明式 | ✅ 是 | ❌ 否 |
| 性能 | ✅ 预加载 | ⚠️ 需要手动 |
| 简单性 | ✅ 简单 | ⚠️ 需要逻辑 |
3.2 Link 组件
'use client';
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/artifact/123">工件详情</Link>
{/* 带查询参数 */}
<Link href="/search?q=hello">搜索</Link>
{/* 替换当前页面 */}
<Link href="/login" replace>登录</Link>
{/* 编程式导航 */}
<Link href={{ pathname: '/post', query: { id: '123' } }}>
文章详情
</Link>
</nav>
);
}3.3 useRouter Hook
'use client';
import { useRouter } from 'next/navigation';
export default function LoginButton() {
const router = useRouter();
const handleLogin = async () => {
// 登录逻辑
const success = await login();
if (success) {
// 登录成功后跳转
router.push('/dashboard');
}
};
return <button onClick={handleLogin}>登录</button>;
}4️⃣ 本项目的导航模式
4.1 路由保护
查看实现:app/components/ProtectedRoute.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useAuth } from '@/app/contexts/AuthContext';
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const pathname = usePathname();
// 公开路由
const publicRoutes = ['/login'];
if (loading) return <div>加载中...</div>;
if (!user && !publicRoutes.includes(pathname)) {
// 未登录,不渲染子组件
return null;
}
return <>{children}</>;
}4.2 会话切换
// 在 useSessionManager 中
const switchSession = (session: Session) => {
setCurrentSession(session);
// 根据会话类型跳转
if (session.type === 'deepresearch') {
router.push('/deepresearch');
} else {
router.push('/');
}
};5️⃣ 最佳实践
5.1 Hook 命名规范
// ✅ 好的命名
useCounter();
useFetch();
useAuth();
useSessionManager();
// ❌ 不好的命名
counter();
fetchData();
auth();
getSession();5.2 Hook 使用规则
必须遵循的规则:
- ✅ 只在组件顶层调用 Hook
- ✅ 只在 React 函数中调用 Hook
- ✅ 自定义 Hook 也以
use开头
示例:
// ✅ 正确
export default function Component() {
const [count, setCount] = useState(0);
const { user } = useAuth();
useEffect(() => {
// ...
}, []);
}
// ❌ 错误
export default function Component() {
if (condition) {
const [count, setCount] = useState(0); // 不能在条件中调用
}
}
// ❌ 错误
function notAComponent() {
const [count, setCount] = useState(0); // 不能在非 React 函数中调用
}5.3 性能优化
'use client';
import { useState, useCallback, useMemo } from 'react';
export function useOptimizedHook() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// 使用 useCallback 缓存函数
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
// 使用 useMemo 缓存计算结果
const doubled = useMemo(() => count * 2, [count]);
return { count, increment, doubled, data };
}💡 练习题
-
选择题:以下哪个 Hook 用于编程式导航?
- A. usePathname
- B. useRouter
- C. useSearchParams
- D. useLink
-
代码题:创建一个自定义 Hook
useWindowSize(),返回当前窗口大小。 -
分析题:查看本项目的 app/hooks/useSessionManager.ts,说明它如何管理会话状态。
-
实践题:创建一个自定义 Hook
useFetch(url),封装数据获取逻辑。
📚 参考资源
官方文档
本项目相关文件
- app/hooks/useSessionManager.ts
- app/hooks/useChatMessages.ts
- app/hooks/useSendMessage.ts
- app/components/ProtectedRoute.tsx
✅ 总结
Next.js 内置 Hooks:
useRouter- 编程式导航usePathname- 获取当前路径useSearchParams- 获取查询参数
自定义 Hooks:
- 封装可复用逻辑
- 以
use开头 - 只在组件顶层调用
导航方式:
Link组件 - 声明式导航useRouter- 编程式导航
本项目的 Hooks:
useSessionManager- 会话管理useChatMessages- 消息管理useSendMessage- 消息发送useAuth- 认证状态
下一步:阅读下一篇文章《中间件与认证模式》,学习 Next.js 的认证实现。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。