在 React 应用里,侧边栏通常是页面布局的核心组成部分。当用户点击不同的菜单项时,希望对应的图标与文字颜色发生变化,从而明确当前所在模块,这是很基础也很实用的交互需求。实现这个功能的关键在于把“当前选中项”作为组件状态来管理,并基于该状态动态渲染样式。

为什么不能直接操作 DOM 修改颜色
一些初学者在 jQuery 时代习惯用点击事件直接找到节点改色,但在 React 中这种做法并不可取。React 的核心思想是声明式渲染,界面应该由状态(state)驱动。如果你在事件处理函数里用 document.querySelector 修改了某个菜单的颜色,当下一次组件因任何原因重新渲染时,React 会根据它自己维护的虚拟 DOM 覆盖掉你手动改的样式,导致颜色“闪一下又恢复”。
此外,直接操作 DOM 会让组件状态与界面表现不一致,后期维护时很难追踪某个菜单为什么是高亮态。当侧边栏需要支持路由跳转、权限控制或折叠展开时,这种写法会迅速失控。因此,我们应当放弃命令式思路,转向用状态控制类名或样式对象。
使用 useState 管理当前选中项
最直观的方案是在侧边栏组件里用 useState 保存一个标识当前选中菜单的 key。点击菜单时,调用 setActiveKey 更新状态,组件重新渲染,所有菜单根据 activeKey 判断自己是否选中,从而应用对应的颜色样式。
下面是一段简化但完整的示例代码,使用内联样式切换文字与图标颜色:
import React, { useState } from 'react';
// 模拟菜单数据
const menuList = [
{ key: 'home', label: '首页', icon: '🏠' },
{ key: 'user', label: '用户', icon: '👤' },
{ key: 'setting', label: '设置', icon: '⚙️' }
];
function Sidebar() {
const [activeKey, setActiveKey] = useState('home');
return (
<div style={{ width: 200, padding: 10 }}>
{menuList.map(item => {
const isActive = item.key === activeKey;
const color = isActive ? '#1890ff' : '#333';
return (
<div
key={item.key}
onClick={() => setActiveKey(item.key)}
style={{
cursor: 'pointer',
padding: '8px 12px',
color: color,
fontSize: 14
}}
>
<span style={{ marginRight: 8, color: color }}>{item.icon}</span>
{item.label}
</div>
);
})}
</div>
);
}
export default Sidebar;
上述代码中,activeKey 就是控制高亮的核心状态。每个菜单项的 color 变量根据 isActive 计算,图标和文字使用同一颜色值,保证了视觉一致。点击时只改状态,不碰 DOM,完全符合 React 设计理念。
结合 CSS 类名实现更灵活的样式
内联样式适合简单场景,但实际项目中通常用 CSS 文件定义选中态样式,便于统一主题。我们可以为菜单项动态添加 active 类名,在 CSS 中写清图标与文字颜色。
示例 CSS 与组件改造如下:
.menu-item {
padding: 8px 12px;
color: #333;
cursor: pointer;
font-size: 14px;
}
.menu-item.active {
color: #1890ff;
}
.menu-item .icon {
margin-right: 8px;
}
.menu-item.active .icon {
color: #1890ff;
}
import React, { useState } from 'react';
import './sidebar.css';
const menuList = [
{ key: 'home', label: '首页', icon: '🏠' },
{ key: 'user', label: '用户', icon: '👤' },
{ key: 'setting', label: '设置', icon: '⚙️' }
];
function Sidebar() {
const [activeKey, setActiveKey] = useState('home');
return (
<div className="sidebar">
{menuList.map(item => (
<div
key={item.key}
className={'menu-item' + (item.key === activeKey ? ' active' : '')}
onClick={() => setActiveKey(item.key)}
>
<span className="icon">{item.icon}</span>
{item.label}
</div>
))}
</div>
);
}
export default Sidebar;
使用类名方案后,如果设计稿要求选中态文字加粗、图标放大,只需改 CSS,不必动 JS 逻辑。团队开发中,样式与行为分离也更利于协作。
使用 SVG 图标组件时的颜色控制
如果项目用的是 SVG 图标(如 Ant Design 的 @ant-design/icons),图标本身是组件,可通过 style 或 fill 属性接收颜色。下面演示传入当前颜色:
import React, { useState } from 'react';
import { HomeOutlined, UserOutlined, SettingOutlined } from '@ant-design/icons';
const menuList = [
{ key: 'home', label: '首页', Icon: HomeOutlined },
{ key: 'user', label: '用户', Icon: UserOutlined },
{ key: 'setting', label: '设置', Icon: SettingOutlined }
];
function Sidebar() {
const [activeKey, setActiveKey] = useState('home');
return (
<div>
{menuList.map(item => {
const isActive = item.key === activeKey;
const color = isActive ? '#1890ff' : '#333';
const ItemIcon = item.Icon;
return (
<div
key={item.key}
onClick={() => setActiveKey(item.key)}
style={{ color, cursor: 'pointer', padding: 8 }}
>
<ItemIcon style={{ color, marginRight: 8 }} />
{item.label}
</div>
);
})}
</div>
);
}
export default Sidebar;
这里把图标组件存进 Icon 属性,渲染时赋给 ItemIcon 并传 style。由于 Ant Design 图标默认使用 currentColor,父级 div 的 color 也会透传,所以即使不单独给图标设色也能生效。若需更精细控制,显式传 color 是最稳妥的。
与路由联动的进阶做法
当侧边栏需要配合 React Router 时,可以用 useLocation 读取当前路径作为 activeKey,而非仅用 useState。这样刷新页面或直链进入子页面,高亮依然正确。
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const menuList = [
{ key: '/home', label: '首页' },
{ key: '/user', label: '用户' },
{ key: '/setting', label: '设置' }
];
function Sidebar() {
const location = useLocation();
const navigate = useNavigate();
return (
<div>
{menuList.map(item => {
const isActive = location.pathname === item.key;
return (
<div
key={item.key}
onClick={() => navigate(item.key)}
style={{
color: isActive ? '#1890ff' : '#333',
cursor: 'pointer',
padding: 8
}}
>
{item.label}
</div>
);
})}
</div>
);
}
export default Sidebar;
这种写法把选中态来源从内部状态变为路由地址,适合大多数后台系统。如果菜单 key 与路由不完全一致,可在数据里加 path 字段做映射。总之,无论哪种方式,核心都在于“单一数据源驱动视图”,避免手工改 DOM。
小结与避坑提醒
实现侧边栏菜单点击切换图标与文字颜色,本质是用状态记录选中项,并以该状态计算样式。新手常踩的坑包括:用 ref 或原生事件直接改色、在 map 里写死 index 当作 key、把图标当背景图无法继承颜色等。只要坚持声明式渲染,把交互转为状态更新,这类需求都能用几十行代码稳定解决。
如果后续侧边栏变复杂,比如要支持折叠、权限过滤、多层级,建议把菜单数据和选中逻辑抽成独立 hook,例如 useSidebar,保持组件干净。也可以引入 Context 让深层子菜单共享 activeKey,不必一层层传 props。