你的React应用不需要那么多JavaScript
当我第一次在React里做暗黑模式切换时,我确信自己做错了什么。难道光是换几种颜色就需要这么多代码吧。

React 的特点就是:它设计用来构建复杂的网页应用,这也让简单功能很容易被过度设计。
在原版JavaScript中,主题切换器可能需要四五行才能检测到点击并切换类。在React中,你可以快速获得“保留主题”、一个上下文提供者通过应用传递、修复:iPhone 屏幕自动变暗 - 致知笔记 - 专注教程知识分享 每次切换重新渲染,以及加载时处理的水合闪烁。body``useState
本文的目标是找出那些React为浏览器已能处理的功能增加复杂性的地方,然后用匹配的HTML或CSS功能替代这些额外的JavaScript。
以下是我们将关注的六种模式:
| 模式 | 内置替代方案 |
|---|---|
| 暗黑模式 | :has()+ 一个复选框 |
| 手风琴 | / |
| 模态 | 弹出API / |
| 旋转木马 | scroll-snap + ::scroll-button() + ::scroll-marker |
| 自定义选择 | appearance: base-select |
| 滚动动画 | animation-timeline: view() |
本文基于我在2026年React峰会上同名演讲。
我们先从触发这一切的开关说起。
无上下文提供者的暗黑模式
如果我们写出React中暗黑模式切换的实际代码,可以这样:
const ThemeContext = createContext<ThemeCtx | null>(null);
function ReactThemeProvider({ children }) {
const [theme, setTheme] = useState<Theme>('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved as Theme);
}, []);
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
const toggle = useCallback(
() => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
[]
);
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
这种做法有效,但实际上远远超出了工作本身的要求。主题切换器需要更新页面样式。大多数组件不需要知道选择了哪个主题;他们只需要正确的样式就能应用。
我们可以把这项责任转移到HTML和CSS中:
// HTML
<label className="switch">
<input type="checkbox" id="theme-switcher" />
<span>Toggle dark mode</span>
</label>
:root {
--bg: #fff;
--text: #000;
color-scheme: light;
}
:root:has(#theme-switcher:checked) {
--bg: #000;
--text: #fff;
color-scheme: dark;
}
选择器允许我们根据父节点的后代状态来为其样式,而这在CSS中长期以来是不可能的。在这里,我们用它来在勾选复选框时更改根级主题变量。:has()
color-scheme它为我们处理了主题的另一部分。修复:无法在 iPhone 上删除某些照片 - 致知笔记 - 专注教程知识分享 它会告诉浏览器页面使用的颜色方案,因此当用户切换主题时,表单控件和滚动条等原生界面可以自动适应。
为什么更好
-
没有上下文提供者或钩子;样式逻辑保持在样式表中
-
切换主题不会导致React组件重新渲染
-
组件通过CSS继承了主动主题,而不是通过prop接收
你可以在CodePen上观看暗黑模式的演示。
这就是她写的全部。这个例子触及了文章的核心思想:让浏览器完成它已经会做的工作。
这并不意味着我们可以完全移除JavaScript。CSS 可以处理可视化状态,但它无法单独记住用户在访问中的选择。如果我们想持久化所选主题或退回到用户的系统偏好,JavaScript 仍然有其实用功能:
// JS
const savedTheme = localStorage.getItem('theme');
const initialTheme =
savedTheme ??
(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
// HTML
<label className="switch">
<input
type="checkbox"
id="theme-switcher"
defaultChecked={initialTheme === 'dark'}
onChange={(e) =>
localStorage.setItem('theme', e.target.checked ? 'dark' : 'light')
}
/>
<span>Toggle dark mode</span>
</label>
这里,我们首先检查 中存储的主题。如果没有初始值,我们会用媒体查询检查用户的系统设置,并从中选择初始值。localStorage``prefers-color-scheme
同样的原则也适用于我们接下来要看的其他模式:如何从 Apple ID 和 iPhone 中删除信用卡 - 致知笔记 - 专注教程知识分享让HTML和CSS尽可能多地处理交互和展示,然后用JavaScript处理它们无法处理的部分。
内置浏览器手风琴
构建手风琴很容易变成整个组件的状态和事件处理程序:
function AccordionItem() {
const [open, setOpen] = useState(false);
return (
<div>
<button
aria-expanded={open}
onClick={() => setOpen(!open)}
>
What is an accordion?
</button>
{open && (
<div>This is an accordion</div>
)}
</div>
);
}
或者它可以是两个HTML元素:
function AccordionItem() {
return (
<details>
<summary>Is an accordion a type of animal?</summary>
No, that's an armadillo.
</details>
);
}
为什么更好
-
不需要状态或点击处理程序
-
内置键盘与焦点行为
-
浏览器功能可以与隐藏内容兼容;例如,Chromium的页面内查找功能可以发现封闭元素中的匹配内容并自动打开
注:在 React 中,将元素与处理器并列,组件挂载时触发切换事件。如果那个初始事件会触发应用逻辑,你可以用“已挂载”的引用来防范。open<details>onToggle
模态的原生替代
模态比看起来复杂得多。把内容放在页面顶部是最简单的部分。你还需要考虑背景、对焦管理、对焦回到扳机、键盘操作,以及模态背后的页面是否能滚动。
这也是开发者常常选择库的原因。浏览器现在为许多这些用例提供了两种原生选项。

浙公网安备 33010602011771号