你的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

模态的原生替代

模态比看起来复杂得多。把内容放在页面顶部是最简单的部分。你还需要考虑背景、对焦管理、对焦回到扳机、键盘操作,以及模态背后的页面是否能滚动。

这也是开发者常常选择库的原因。浏览器现在为许多这些用例提供了两种原生选项。

posted @ 2026-09-14 20:47  panxianren  阅读(5)  评论(0)    收藏  举报