You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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>
  );
}

为什么这个方案能解决你的问题

  1. 避免Hydration失败:服务端预构建好完整的树形数据,客户端直接渲染,服务端和客户端的DOM结构完全一致,不会出现不匹配的情况。
  2. 支持任意层级:递归组件可以处理无限深度的分类嵌套,不管是Main->Child还是Main->Child->ChildChild->ChildChildChild都能正确渲染。
  3. 性能优化:仅执行一次数据库查询,后续通过内存Map构建树形,避免了渲染时多次调用find的低效操作。
  4. 无重复显示:每个分类仅在树形结构中出现一次,通过唯一的_id作为组件key,确保渲染唯一。

内容的提问来源于stack exchange,提问作者Dragos123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 12:16:30