useMediaQuery:React 响应式设计完全指南

CSS 媒体查询能处理大部分响应式布局工作,但有时你需要在 JavaScript 层面让 React 组件感知当前的视口、用户偏好或设备能力。无论是条件渲染移动端导航、检测深色模式,还是尊重减少动效偏好,useMediaQuery 都能给你一个与任意 CSS 媒体查询字符串保持同步的响应式布尔值。

什么是 useMediaQuery?

useMediaQueryReactUse 提供的一个 Hook,它封装了浏览器的 window.matchMedia API。传入一个媒体查询字符串,返回一个布尔值表示该查询当前是否匹配。它在底层订阅了 change 事件,因此当用户调整窗口大小、切换系统深色模式或改变查询描述的任何条件时,返回值会自动更新。

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function Example() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return <p>{isMobile ? "移动端视图" : "桌面端视图"}</p>;
}

函数签名非常简洁:

tsx 复制代码
useMediaQuery(query: string, defaultState?: boolean) => boolean
  • query --- 任意有效的 CSS 媒体查询字符串。
  • defaultState --- 可选的布尔值,用于服务端渲染时 window 不可用的情况。

基本用法

最常见的场景是检测屏幕宽度断点:

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function Navigation() {
  const isMobile = useMediaQuery("(max-width: 767px)");

  if (isMobile) {
    return (
      <button aria-label="打开菜单">
        <HamburgerIcon />
      </button>
    );
  }

  return (
    <nav>
      <a href="/">首页</a>
      <a href="/about">关于</a>
      <a href="/contact">联系</a>
    </nav>
  );
}

组件仅在布尔值变化时重新渲染------而非窗口每移动一个像素都触发。

常用断点模式

对于使用多个断点的项目,在一处定义并在各组件间复用:

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function useBreakpoint() {
  const isMobile = useMediaQuery("(max-width: 639px)");
  const isTablet = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
  const isDesktop = useMediaQuery("(min-width: 1024px)");

  return { isMobile, isTablet, isDesktop };
}

function Dashboard() {
  const { isMobile, isTablet } = useBreakpoint();

  const columns = isMobile ? 1 : isTablet ? 2 : 4;

  return (
    <div style={{ display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: 16 }}>
      <Card title="收入" />
      <Card title="用户" />
      <Card title="订单" />
      <Card title="增长" />
    </div>
  );
}

响应式布局

以下是一个实际示例,在桌面端显示侧边栏布局,在移动端显示堆叠布局:

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function AppLayout({ children }: { children: React.ReactNode }) {
  const isWide = useMediaQuery("(min-width: 1024px)");

  if (isWide) {
    return (
      <div style={{ display: "flex" }}>
        <aside style={{ width: 260, flexShrink: 0 }}>
          <SidebarMenu />
        </aside>
        <main style={{ flex: 1 }}>{children}</main>
      </div>
    );
  }

  return (
    <div>
      <TopNavBar />
      <main>{children}</main>
    </div>
  );
}

检测用户偏好

媒体查询不限于屏幕尺寸。你还可以检测系统级用户偏好:

深色模式

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");

  return (
    <div style={{
      background: prefersDark ? "#1a1a2e" : "#ffffff",
      color: prefersDark ? "#e0e0e0" : "#1a1a1a",
      minHeight: "100vh",
    }}>
      {children}
    </div>
  );
}

减少动效

尊重 prefers-reduced-motion 对无障碍性至关重要。有晕动症或前庭功能障碍的用户会在操作系统层面设置此偏好:

tsx 复制代码
import { useMediaQuery } from "@reactuses/core";

function AnimatedCard({ children }: { children: React.ReactNode }) {
  const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");

  return (
    <div style={{
      transition: prefersReducedMotion ? "none" : "transform 0.3s ease",
    }}>
      {children}
    </div>
  );
}

高对比度及其他查询

tsx 复制代码
const prefersHighContrast = useMediaQuery("(prefers-contrast: high)");
const isPortrait = useMediaQuery("(orientation: portrait)");
const hasHover = useMediaQuery("(hover: hover)");

SSR 与 Hydration 安全

在服务端渲染时,window.matchMedia 不存在。如果不提供 defaultState,Hook 在服务端返回 false,在客户端返回真实值,这可能导致 React hydration 不匹配的警告。

为避免此问题,传入一个与大多数用户预期相匹配的 defaultState

ini 复制代码
// 服务端渲染为 false,客户端更新为真实值
const isMobile = useMediaQuery("(max-width: 768px)", false);

// 服务端渲染为 true,适用于大部分流量来自移动端的场景
const isMobile = useMediaQuery("(max-width: 768px)", true);

在开发模式下,如果你在服务端渲染时未提供 defaultState,Hook 会在控制台输出警告,提醒你显式处理这种情况。

与其他 Hooks 组合

useMediaQuery 与其他 ReactUse Hook 搭配使用效果很好:

tsx 复制代码
import { useMediaQuery, useLocalStorage } from "@reactuses/core";

function ThemeSwitcher() {
  const systemPrefersDark = useMediaQuery("(prefers-color-scheme: dark)");
  const [userTheme, setUserTheme] = useLocalStorage<"light" | "dark" | "system">("theme", "system");

  const isDark = userTheme === "system" ? systemPrefersDark : userTheme === "dark";

  return (
    <div>
      <p>当前主题:{isDark ? "深色" : "浅色"}</p>
      <select value={userTheme} onChange={(e) => setUserTheme(e.target.value as "light" | "dark" | "system")}>
        <option value="system">跟随系统</option>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
    </div>
  );
}

常见错误

在渲染中直接使用 window.matchMedia。 在渲染期间调用 window.matchMedia 而不订阅变化,只能得到一个过时的快照。useMediaQuery 订阅了 change 事件,保证值始终是最新的。

SSR 时忘记 defaultState。 如果你使用 Next.js、Remix 或 Astro,务必传入 defaultState 以防止 hydration 警告。

创建过多监听器。 每次调用 useMediaQuery 会创建一个 matchMedia 监听器。虽然这很轻量,但如果你需要几十个查询,考虑将相关断点归入一个自定义 Hook(如上面的 useBreakpoint)。

安装

css 复制代码
npm i @reactuses/core

或使用其他包管理器:

sql 复制代码
pnpm add @reactuses/core
# 或
yarn add @reactuses/core

相关 Hooks


ReactUse 提供了 100 多个 React Hooks。查看全部 →

相关推荐
小金鱼Y1 小时前
一文吃透 JavaScript 防抖:从原理到实战,让你的页面不再 “手抖”
前端·javascript·面试
Z兽兽1 小时前
React 18 开发环境下useEffect 会执行两次,原因分析及解决方案
前端·react.js·前端框架
紫_龙1 小时前
最新版vue3+TypeScript开发入门到实战教程之Vue3详解props
前端·vue.js·typescript
树上有只程序猿2 小时前
这波低代码热,能维持多久
前端
姓王名礼2 小时前
这是一个完整的全栈交付包,包含Vue3 前端交互界面(集成数字人视频流、ECharts 图表、语音对话)和Docker Compose 一键部署脚本。
前端·docker·echarts
嵌入式-老费2 小时前
vivado hls的应用(axis接口)
前端·webpack·node.js
孟陬2 小时前
国外技术周刊第 2 期 — 本周热门 🔥 YouTube 视频 TED 演讲 AI 如何能够拯救(而非摧毁)教育
前端·后端·程序员
小飞大王6662 小时前
从零手写 React:深度解析 Fiber 架构与 Hooks 实现
前端·react.js·架构
进击的尘埃2 小时前
Nginx 反向代理 WebSocket 和 SSE 的踩坑
javascript