如何在Next.js+MongoDB中动态展示多级父子分类菜单?
解决方案:递归构建分类树形结构 + 服务端预取数据
核心思路
一次性从MongoDB拉取全部分类数据,在内存中递归构建树形结构,再通过Next.js服务端预取(getServerSideProps/getStaticProps)传递给客户端组件,彻底避免客户端渲染时重复查询数据库的问题,同时解决Hydration不匹配的问题(服务端和客户端使用完全一致的树形数据)。
步骤1:构建树形结构的工具函数
利用MongoDB分类的parent字段(唯一ObjectId)构建树形,时间复杂度O(n),性能高效:
/** * 将扁平化的分类数组转为树形结构 * @param {Array} categories 从MongoDB查询到的全部分类数组 * @returns {Array} 根分类数组(包含嵌套的children) */ function buildCategoryTree(categories) { const categoryMap = new Map(); const rootCategories = []; // 1. 先将所有分类存入Map,用_id作为键,方便快速查找 categories.forEach(category => { categoryMap.set(category._id.toString(), { ...category, children: [] }); }); // 2. 遍历分类,将子分类挂载到对应父分类的children数组中 categories.forEach(category => { if (category.parent) { const parentId = category.parent.toString(); const parentCategory = categoryMap.get(parentId); if (parentCategory) { parentCategory.children.push(categoryMap.get(category._id.toString())); } } else { // parent为null的是根分类 rootCategories.push(categoryMap.get(category._id.toString())); } }); return rootCategories; }
步骤2:Next.js服务端预取数据
在页面组件中通过getServerSideProps(或getStaticProps,根据是否需要静态生成)预取全部分类并构建树形:
import { CategoryModel } from '@/models/Category'; export async function getServerSideProps() { // 一次性拉取全部分类,用lean()返回普通JS对象,提升性能和序列化兼容性 const categories = await CategoryModel.find({}).lean(); // 构建树形结构 const categoryTree = buildCategoryTree(categories); return { props: { // 序列化处理MongoDB特殊类型(如ObjectId) categoryTree: JSON.parse(JSON.stringify(categoryTree)) } }; }
步骤3:递归渲染分类组件
编写递归组件渲染任意层级的分类,完全匹配你需要的格式:
// 递归渲染单个分类项 function CategoryItem({ category, level = 0 }) { // 根据层级计算缩进 const indent = level > 0 ? ' '.repeat(level * 3) : ''; // 生成前缀:根分类用粗体,子分类用带缩进的-> const displayText = level === 0 ? `**${category.name}**` : `${indent}-> ${category.name}`; return ( <div> <p>{displayText}</p> // 递归渲染子分类 {category.children.length > 0 && ( <div> {category.children.map(child => ( <CategoryItem key={child._id} category={child} level={level + 1} /> ))} </div> )} </div> ); } // 页面主组件 export default function CategoriesPage({ categoryTree }) { return ( <div className="category-list"> {categoryTree.map(rootCategory => ( <CategoryItem key={rootCategory._id} category={rootCategory} /> ))} </div> ); }
为什么这个方案能解决你的问题
- 避免Hydration失败:服务端预构建好完整的树形数据,客户端直接渲染,服务端和客户端的DOM结构完全一致,不会出现不匹配的情况。
- 支持任意层级:递归组件可以处理无限深度的分类嵌套,不管是
Main->Child还是Main->Child->ChildChild->ChildChildChild都能正确渲染。 - 性能优化:仅执行一次数据库查询,后续通过内存Map构建树形,避免了渲染时多次调用
find的低效操作。 - 无重复显示:每个分类仅在树形结构中出现一次,通过唯一的
_id作为组件key,确保渲染唯一。
内容的提问来源于stack exchange,提问作者Dragos123
相关产品推荐
相关产品推荐

