Learn

/

Conditional Rendering

Conditional Rendering

5 patterns

Rendering different components by viewport vs hiding with CSS, including performance and SEO tradeoffs. You'll hit this when you conditionally render with JavaScript but the hidden component still makes a network request.

Avoid
function Nav() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  return isMobile
    ? <MobileNav />
    : <DesktopNav />;
}
function Nav() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  return isMobile
    ? <MobileNav />
    : <DesktopNav />;
}

Prefer
function Nav() {
  return (
    <>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <MobileNav />
      </Box>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <DesktopNav />
      </Box>
    </>
  );
}
function Nav() {
  return (
    <>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <MobileNav />
      </Box>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <DesktopNav />
      </Box>
    </>
  );
}
Why avoid

useMediaQuery returns false during SSR. On a mobile device, the server sends DesktopNav, then React hydrates and swaps to MobileNav, causing a visible flash. CSS display toggling eliminates this entirely.

Why prefer

Rendering both and toggling with CSS display avoids the hydration flash from useMediaQuery. Both components exist in the DOM (good for SEO and accessibility), and the switch is instant because no JavaScript needs to run.

MUI: Hiding elements
Avoid
function Dashboard() {
  return (
    <>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <MobileDashboard />
      </Box>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <DesktopDashboard />
      </Box>
    </>
  );
}

// Both dashboards fetch data independently
// Both run expensive chart calculations
function Dashboard() {
  return (
    <>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <MobileDashboard />
      </Box>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <DesktopDashboard />
      </Box>
    </>
  );
}

// Both dashboards fetch data independently
// Both run expensive chart calculations

Prefer
"use client";

function Dashboard() {
  const isDesktop = useMediaQuery("(min-width: 900px)", {
    defaultMatches: true,
  });

  return isDesktop
    ? <DesktopDashboard />
    : <MobileDashboard />;
}
"use client";

function Dashboard() {
  const isDesktop = useMediaQuery("(min-width: 900px)", {
    defaultMatches: true,
  });

  return isDesktop
    ? <DesktopDashboard />
    : <MobileDashboard />;
}
Why avoid

Rendering both dashboards means both fetch data, both calculate charts, and both build their DOM trees. Only one is visible. For lightweight UI differences, CSS display is better, but for expensive components, conditional rendering saves real resources.

Why prefer

When both versions are expensive (data fetching, chart rendering, heavy DOM), rendering both wastes resources. useMediaQuery with defaultMatches: true reduces the SSR flash. For heavy components, the hydration tradeoff is worth avoiding double the work.

MUI: useMediaQuery SSR
Avoid
function Filters() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  if (isMobile) return null;

  return (
    <Stack spacing={2}>
      <CategoryFilter />
      <PriceFilter />
      <RatingFilter />
    </Stack>
  );
}
function Filters() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  if (isMobile) return null;

  return (
    <Stack spacing={2}>
      <CategoryFilter />
      <PriceFilter />
      <RatingFilter />
    </Stack>
  );
}

Prefer
function Filters() {
  return (
    <>
      {/* Desktop: always visible */}
      <Stack
        spacing={2}
        sx={{ display: { xs: "none", md: "flex" } }}
      >
        <CategoryFilter />
        <PriceFilter />
        <RatingFilter />
      </Stack>

      {/* Mobile: behind a drawer */}
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <FilterDrawer>
          <CategoryFilter />
          <PriceFilter />
          <RatingFilter />
        </FilterDrawer>
      </Box>
    </>
  );
}
function Filters() {
  return (
    <>
      {/* Desktop: always visible */}
      <Stack
        spacing={2}
        sx={{ display: { xs: "none", md: "flex" } }}
      >
        <CategoryFilter />
        <PriceFilter />
        <RatingFilter />
      </Stack>

      {/* Mobile: behind a drawer */}
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <FilterDrawer>
          <CategoryFilter />
          <PriceFilter />
          <RatingFilter />
        </FilterDrawer>
      </Box>
    </>
  );
}
Why avoid

Removing filters on mobile means mobile users can't filter results at all. Responsive design isn't about removing features. It's about presenting them appropriately for the device. A drawer or bottom sheet is the mobile pattern for filters.

Why prefer

Hiding filters entirely on mobile removes functionality users need. A drawer provides the same filters in a mobile-friendly pattern called progressive disclosure. The filters are always accessible, just presented differently.

Material Design: Navigation drawer
Avoid
function ProductPage() {
  return (
    <>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <Header />
        <ProductGrid layout="horizontal" />
        <Sidebar />
        <Footer />
      </Box>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <Header />
        <ProductGrid layout="vertical" />
        <Footer />
      </Box>
    </>
  );
}
function ProductPage() {
  return (
    <>
      <Box sx={{ display: { xs: "none", md: "block" } }}>
        <Header />
        <ProductGrid layout="horizontal" />
        <Sidebar />
        <Footer />
      </Box>
      <Box sx={{ display: { xs: "block", md: "none" } }}>
        <Header />
        <ProductGrid layout="vertical" />
        <Footer />
      </Box>
    </>
  );
}

Prefer
function ProductPage() {
  return (
    <>
      <Header />
      <Stack direction={{ xs: "column", md: "row" }}>
        <ProductGrid
          sx={{
            flex: 1,
            "& .product-card": {
              flexDirection: { xs: "column", md: "row" },
            },
          }}
        />
        <Sidebar
          sx={{ display: { xs: "none", md: "block" } }}
        />
      </Stack>
      <Footer />
    </>
  );
}
function ProductPage() {
  return (
    <>
      <Header />
      <Stack direction={{ xs: "column", md: "row" }}>
        <ProductGrid
          sx={{
            flex: 1,
            "& .product-card": {
              flexDirection: { xs: "column", md: "row" },
            },
          }}
        />
        <Sidebar
          sx={{ display: { xs: "none", md: "block" } }}
        />
      </Stack>
      <Footer />
    </>
  );
}
Why avoid

Two complete page trees means Header, ProductGrid, and Footer are mounted twice, resulting in double the DOM nodes, double the event listeners, and double the data fetching. Any bug fix or feature change must be applied to both copies.

Why prefer

Share the component tree and use responsive props for the differences. Duplicating the entire page means every change needs updating in two places, event handlers fire twice, and the DOM is much larger. Only split when the differences are truly fundamental.

MUI: Responsive values
Avoid
import MobileEditor from "./MobileEditor";
import DesktopEditor from "./DesktopEditor";

function EditorPage() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return isMobile ? <MobileEditor /> : <DesktopEditor />;
}

// Both editors are in the main bundle
import MobileEditor from "./MobileEditor";
import DesktopEditor from "./DesktopEditor";

function EditorPage() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return isMobile ? <MobileEditor /> : <DesktopEditor />;
}

// Both editors are in the main bundle

Prefer
import dynamic from "next/dynamic";

const MobileEditor = dynamic(
  () => import("./MobileEditor"),
);
const DesktopEditor = dynamic(
  () => import("./DesktopEditor"),
  { ssr: false },
);

function EditorPage() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  return isMobile ? <MobileEditor /> : <DesktopEditor />;
}

// Only the needed editor is downloaded
import dynamic from "next/dynamic";

const MobileEditor = dynamic(
  () => import("./MobileEditor"),
);
const DesktopEditor = dynamic(
  () => import("./DesktopEditor"),
  { ssr: false },
);

function EditorPage() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  return isMobile ? <MobileEditor /> : <DesktopEditor />;
}

// Only the needed editor is downloaded
Why avoid

Static imports bundle both editors into the main JavaScript file. Mobile users download the entire desktop editor they'll never use. For heavy, device-specific components, dynamic imports save significant bundle size.

Why prefer

Dynamic imports with next/dynamic code-split each editor into its own chunk. Mobile users only download the mobile editor bundle. This is the correct use of useMediaQuery: when you need to avoid *loading* heavy code, not just hiding it with CSS. The hydration flash tradeoff is acceptable here because the alternative (loading both heavy bundles) is worse.

Next.js: Lazy loading