Next.js 标签与分类系统问题
开发博客系统的过程中,我遇到的几个问题
在开发博客系统的过程中,我遇到了几个问题:
- 点击标签没有任何反应
- 分类页无法根据 URL 参数自动筛选
- 控制台报错:Cannot read properties of undefined
- 标签字段既可能是字符串,也可能是数组
本文将完整拆解问题成因,并结合关键代码,深入讲解技术细节与设计思路。
一、问题一:点击标签没反应
原因分析
最初的实现是:
const [selectedTag, setSelectedTag] = useState("全部");
<TagItem
onClick={() => setSelectedTag(tag)}
/>
问题在于:
- 只是更新了 React 本地状态
- 没有做页面跳转
- 没有筛选逻辑
所以点击后 UI 没变化,看起来“没有反应”。
正确做法:使用 useRouter 进行跳转
在 Next.js App Router 中,客户端跳转需要使用:
import { useRouter } from "next/navigation";
核心修改:
const router = useRouter();
<TagItem
onClick={() =>
router.push(`/category?category=${encodeURIComponent(tag)}`)
}
/>
技术细节说明
-
useRouter 只能在 "use client" 组件中使用。
-
encodeURIComponent 必须使用。
如果标签包含:
- 空格
- 中文
- 特殊字符
不编码会导致 URL 解析错误。
二、问题二:分类页无法读取 URL 参数
点击标签后跳转到:
/category?category=React
但页面没有自动筛选。
原因
selectedCategory 默认写死为:
useState("全部")
没有读取 searchParams。
正确实现:useSearchParams
import { useSearchParams } from "next/navigation";
const searchParams = useSearchParams();
const categoryFromUrl = searchParams.get("category");
const [selectedCategory, setSelectedCategory] = useState(
categoryFromUrl ?? "全部"
);
技术原理
App Router 中:
- useSearchParams 是客户端 Hook
- 每次 URL 变化会触发重新渲染
这样分类页就可以支持:
- 可分享链接
- 刷新后状态保留
- SEO 友好结构
三、问题三:Cannot read properties of undefined
报错位置:
articles.forEach(...)
原因
articles 在某些渲染周期可能是 undefined。
比如:
- 异步数据还未加载
- 父组件传值错误
防御式编程
const tagMap = useMemo(() => {
if (!articles) return [];
同时在 JSX 中:
count={articles?.length || 0}
技术原则
在 React 中永远不要假设 props 一定存在。
防御式编程可以避免:
- 页面崩溃
- 白屏
- hydration mismatch
四、核心逻辑解析:TagCard 标签统计算法
下面是核心代码:
export default function TagCard({ articles }: TagCardProps) {
const router = useRouter();
const tagMap = useMemo(() => {
if (!articles) return [];
const map = new Map<string, number>();
articles.forEach((article) => {
const rawTags = article.tags;
if (!rawTags) return;
const tagArray =
typeof rawTags === "string"
? rawTags.split(",")
: Array.isArray(rawTags)
? rawTags
: [];
tagArray.forEach((tag) => {
const trimmed = tag.trim();
if (!trimmed) return;
map.set(trimmed, (map.get(trimmed) || 0) + 1);
});
});
return Array.from(map.entries()).sort((a, b) =>
a[0].localeCompare(b[0])
);
}, [articles]);
技术细节拆解
1. useMemo 的意义
useMemo(() => {}, [articles])
作用:
- 只在 articles 变化时重新计算
- 避免每次渲染都重新统计标签
- 优化性能
如果文章数量很多,性能差距会明显。
2. Map 作为统计结构
const map = new Map<string, number>();
优势:
- 键可以是任意字符串
- 不会有原型污染问题
- API 更语义化
统计逻辑:
map.set(tag, (map.get(tag) || 0) + 1);
这是一个典型的计数器模式。
3. 兼容 tags 字段多种类型
typeof rawTags === "string"
Array.isArray(rawTags)
说明后端数据结构可能不稳定。
可能是:
"React,Next.js,TypeScript"
或者:
["React", "Next.js"]
这段代码完成了数据归一化处理。
这是重要的工程实践:在前端做数据兜底。
4. trim 的作用
const trimmed = tag.trim();
避免出现:
" React"
"React "
否则统计会出现两个不同的标签。
5. 排序
.sort((a, b) => a[0].localeCompare(b[0]))
使用 localeCompare 是为了支持:
- 中文排序
- 多语言排序
比直接使用 > 更安全。
五、最终系统结构
现在博客结构已经升级为:
首页 → 点击标签 → /category?category=xxx → 自动筛选 → 可分享链接
它具备:
- URL 状态同步
- 客户端跳转
- 性能优化
- 防御式编程
- 数据结构兼容
这已经是一个接近成熟博客系统的实现方式。
六、工程经验总结
- 状态更新不等于页面跳转。
- URL 是状态的一部分。
- 所有 props 都应进行防御处理。
- 后端数据需要做前端兜底。
- useMemo 是性能优化的重要工具。