React实现按分类分组flex布局渲染的可扩展方案
我当前正在实现数据库数据的映射渲染功能,使用的Mongoose数据模型定义如下:
const itemSchema = new mongoose.Schema({ category: String, name: String, image: String })
后端server.js接口初始代码如下:
app.post('/api', async (req, res) => { Item.find(function (err, result) { if (err) { res.send(err) } else { res.send(result) } }) })
前端React的Home.js组件中,原有代码可以正常渲染所有数据库返回的数据:
<div style={{ display: 'flex' }}> {getBasic.data.map(({ category, image, name, _id }, i) => { return ( <div> <p>{i}</p> <Link to={`/details/${_id}`} state={{ image: image, name: { name }, _id: { _id } }}> <p>{name}</p> <img src={image} /> <p>{_id}</p> <p>{category}</p> </Link> </div> ); })} </div>
当前需求为将渲染内容按category字段分组:所有同分类的商品放在同一个flex容器内横向排列,不同分类的flex容器纵向依次排布,例如Hats分类商品在第一个flex容器,Pants分类在下方第二个flex容器,以此类推。
最初尝试通过两次map遍历实现(先遍历分类,再遍历对应分类下的商品),但不知道如何编写逻辑适配任意数量的分类;也试过添加if (category == 'Hats')这类硬编码判断,但每个分类都要单独写判断逻辑,无扩展性,不是最优方案。
更新
根据建议已经在后端增加了分类排序逻辑,修改后的server.js代码如下:
Item.find({}).sort('category').exec(function (err, result) { if (err) { res.send('error') console.log(err) } else { res.send(result) } })
现在数据已经按category排序返回,但仍未实现分类切换时自动结束当前flex容器、新建下一个分类flex容器的效果,无法做到每个分类单独占一行横向排列。
更新2
目前卡在为每个分类生成独立flex容器的环节:设置display: flex的容器位置在map遍历逻辑外层,无法动态匹配分类。曾尝试如下写法:
<div style={{ display: 'flex' }} className={category}> {getBasic.data.map(({ category, image, name, _id }, i) => { return ( <div> <div> <p>{i}</p> <Link to={`/details/${_id}`} state={{ image: image, name: { name }, _id: { _id } }}> <p>{name}</p> <img src={image} /> <p>{_id}</p> <p>{category}</p> </Link> </div> </div> ) })} </div>
但该写法中className={category}的位置在category变量定义的外层,无法获取到分类值,希望通过调整结构实现每个横向flex容器仅包含对应一个分类的商品。
更新3
需要实现的最终效果和如下硬编码代码的渲染结果完全一致:
<div style={{ display: 'flex' }}> {getBasic.data.map(({ category, image, name, _id }, i) => { if (category == 'Pants') { return ( <div> <div> <p>{i}</p> <Link to={`/details/${_id}`} state={{ image: image, name: { name }, _id: { _id } }}> <p>{name}</p> <img src={image} /> <p>{_id}</p> <p>{category}</p> </Link> </div> </div> ) } })} </div> <div style={{ display: 'flex' }}> {getBasic.data.map(({ category, image, name, _id }, i) => { if (category == 'Hats') { return ( <div> <div> <p>{i}</p> <Link to={`/details/${_id}`} state={{ image: image, name: { name }, _id: { _id } }}> <p>{name}</p> <img src={image} /> <p>{_id}</p> <p>{category}</p> </Link> </div> </div> ) } })} </div>
要求方案可扩展:新增分类(例如coats外套分类)时,无需手动新增div和if判断逻辑,就能自动将新分类的商品渲染到独立的flex横排容器中。
之前双重map写法的问题在于,外层map遍历的是每一条商品数据,而非去重后的分类列表,所以会生成和商品总数等量的flex容器,每个容器又重复渲染全量商品,自然无法实现分组效果。
核心实现思路:先把扁平的商品数组按category字段分组,转成「分类名: 对应商品数组」的结构,再遍历这个分组结构渲染即可,不需要硬编码分类,也不强制依赖后端排序,新增分类会自动适配。
步骤1:实现数据分组
分组逻辑放在if (getBasic)的判断分支内执行,避免接口数据未返回时访问getBasic.data抛出空值错误。两种实现方式任选:
- 原生JS reduce实现(无额外依赖)
const groupedItems = getBasic.data.reduce((groups, item) => { const category = item.category; // 当前分类不存在则初始化空数组 if (!groups[category]) groups[category] = []; // 将当前商品归入对应分类 groups[category].push(item); return groups; }, {}); // 最终数据结构示例:{ Hats: [商品1, 商品2], Pants: [商品3, 商品4], Coats: [...] }
- 若项目引入了Lodash,可直接调用封装好的分组方法
import _ from 'lodash'; const groupedItems = _.groupBy(getBasic.data, 'category');
步骤2:替换原有渲染逻辑
删除之前写的两层嵌套map,替换为遍历分组结构的渲染代码:
{/* 遍历全部分组,每个分类生成独立flex容器 */} {Object.entries(groupedItems).map(([categoryName, categoryItems]) => { return ( <div key={categoryName} style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', margin: '20px 0' }}> {/* 可选:添加分类标题 */} <h3 style={{width: '100%', margin: 0}}>{categoryName}</h3> {/* 渲染当前分类下的所有商品 */} {categoryItems.map(({ image, name, _id }, i) => { return ( <div key={_id}> <p>{i}</p> <Link to={`/details/${_id}`} state={{ image: image, name: { name }, _id: { _id } }}> <p>{name}</p> <img src={image} alt={name} /> <p>{_id}</p> <p>{categoryName}</p> </Link> </div> ) })} </div> ) })}
说明
- 方案完全无硬编码,后端返回多少个分类就自动生成多少个横向flex容器,新增分类无需修改前端代码
- 不依赖后端排序逻辑,哪怕接口返回的分类数据是乱序的,分组逻辑也能正常工作
- 遍历元素添加了
key属性,避免React渲染警告:分类容器用分类名作为key,商品项用数据库唯一标识_id作为key - 示例flex容器添加了
gap控制商品间距、flexWrap: 'wrap'避免商品过多溢出屏幕,可根据自身样式需求调整属性值
内容的提问来源于stack exchange,提问作者Caleb

