后台管理系统的侧边栏几乎都有一个共同交互:用户点击某个菜单项后,该项的图标和文字变成主题色,其余项恢复默认颜色。这个需求看起来简单,但不少刚接触 React 的朋友写出来的代码要么颜色不变,要么所有项一起变色。问题的根源通常出在没有理解“受控”这一概念上——必须有一个唯一的 state 记录“当前选中是谁”,而不是让每个菜单项自己管理自己的颜色。

一、核心思路:用 state 记录选中项,样式由状态驱动
React 的 UI 是状态的映射,侧边栏高亮也不例外。整体思路分三步:第一步,在侧边栏组件中定义一个 state,保存当前选中菜单项的唯一标识,可以是索引,也可以是更具语义的 key 字符串;第二步,遍历菜单配置数组渲染每个菜单项,判断当前项的 key 是否等于 state 中的值;第三步,根据判断结果决定渲染什么样的样式。
推荐用 key 而不是索引作为标识,因为菜单数据一旦发生排序或增删,索引会变化,而 key 更稳定。下面是一个最小可运行示例:
import { useState } from 'react';
const menuItems = [
{ key: 'home', label: '首页', icon: 'icon-home' },
{ key: 'orders', label: '订单管理', icon: 'icon-orders' },
{ key: 'users', label: '用户管理', icon: 'icon-users' },
{ key: 'settings', label: '系统设置', icon: 'icon-settings' },
];
export default function Sidebar() {
const [activeKey, setActiveKey] = useState('home');
return (
<ul className="sidebar-menu">
{menuItems.map(item => (
<li
key={item.key}
className={item.key === activeKey ? 'menu-item active' : 'menu-item'}
onClick={() => setActiveKey(item.key)}
>
<i className={`icon ${item.icon}`}></i>
<span>{item.label}</span>
</li>
))}
</ul>
);
}这里的关键点在 className 的三元表达式:item.key === activeKey 为真时追加 active 类。图标和文字的颜色都由这个类名控制,例如在 CSS 中写 .menu-item.active { color: #1677ff; },再配合 .menu-item.active .icon { color: #1677ff; },就能让图标和文字同步变色。如果图标是 SVG,记得让 SVG 继承父级颜色,即设置 fill: currentColor,否则你会发现文字变色了图标却纹丝不动。
二、三种样式控制方案对比与图标颜色处理
1. 条件 className 方案(推荐)
这是最常见也最易维护的方式。样式全部写在 CSS 文件里,组件只负责切换类名。优点是样式与逻辑分离,方便统一修改主题色;缺点是需要额外维护一份 CSS。注意拼接类名时如果条件变多,建议使用 clsx 这类工具库,避免出现末尾多余空格或 undefined 类名。
2. 内联样式对象方案
如果项目很小或组件库不允许写全局 CSS,可以直接用内联样式:
<li
style={{ color: item.key === activeKey ? '#1677ff' : '#666' }}
onClick={() => setActiveKey(item.key)}
>
{item.icon}
<span>{item.label}</span>
</li>这种写法让文字颜色直接受 state 控制,图标若使用 currentColor 也会跟着变。缺点是无法使用伪类和媒体查询,且样式散落在 JSX 中,后期维护成本较高,仅建议在快速原型阶段使用。
3. CSS Modules 方案
在构建工具支持的前提下,引入样式模块:import styles from './Sidebar.module.css',然后用模板字符串拼接 className={`${styles.menuItem} ${item.key === activeKey ? styles.active : ''}`}。这种方式类名自动哈希,不会污染全局,是中大型项目的首选。
关于图标还有一点要强调:如果你用的是 Ant Design 的 <Icon> 或 iconify 这类组件,它们通常提供 color 或 style 属性,直接绑定即可;如果是自定义 SVG 组件,务必检查 SVG 内部的 path 是否写死了 fill 颜色,写死了就改成 currentColor,否则外部怎么设置颜色都不生效。
三、结合 React Router:让高亮自动跟随路由
手动维护 state 在纯展示型页面没问题,但一旦菜单项对应路由页面,刷新浏览器后 state 会回到初始值,高亮就错位了。正确做法是以路由地址作为唯一数据源。React Router 提供了 useLocation 钩子,当前路径就是它的返回值:
import { NavLink } from 'react-router-dom';
const menuItems = [
{ key: '/home', label: '首页' },
{ key: '/orders', label: '订单管理' },
{ key: '/users', label: '用户管理' },
];
function Sidebar() {
return (
<ul className="sidebar-menu">
{menuItems.map(item => (
<NavLink
key={item.key}
to={item.key}
className={({ isActive }) =>
isActive ? 'menu-item active' : 'menu-item'
}
>
<span>{item.label}</span>
</NavLink>
))}
</ul>
);
}NavLink 的 className 属性支持传入函数,isActive 为真时自动加上 active 类,无需自己写 state,也天然解决了刷新和浏览器前进后退的同步问题。如果不用 NavLink,也可以用 useLocation().pathname 和菜单项的 key 做前缀匹配来判断激活态,但要小心 /users 会误匹配 /users/detail 的情况,此时需要更精确的相等比较或封装匹配函数。
最后补充多级菜单的处理:子菜单高亮时通常需要让父级菜单同时展开并保持高亮。此时可以把 activeKey 设计成数组,或者反过来由当前路由向上查找所属父级 key,再传递给父菜单组件。把选中逻辑收敛在一处,避免父子组件各自为政,是保持代码可维护的关键。