react-device-detector

Render by device. Survive the server.

Device-class rendering for React, with no hydration mismatch and iPadOS detected as a tablet rather than a desktop.

Five view components and a hook. The detector itself is a pure function you can call from server code, so the first paint can already be correct.

Five views, one import

import { MobileView, DesktopView, IOSView, AndroidView, TabletView }
  from "react-device-detector";

<AndroidView>only on Android</AndroidView>
<IOSView>only on iOS</IOSView>
<MobileView>only on mobile</MobileView>
<TabletView>only on tablets</TabletView>
<DesktopView>only on desktop</DesktopView>

Or branch in JavaScript

const { isMobile, isDetecting } = useDevice();
if (isDetecting) return <Skeleton />;
return isMobile ? <MobileNav /> : <DesktopNav />;

isDetecting exists so you can render a skeleton rather than guess.

Correct on the first paint

The main entry ships "use client", so importing the views into a Server Component works as-is. To skip the wait for hydration, detect from the request user-agent instead. The pure detector lives at react-device-detector/server and carries no "use client", so server code can call it.

// app/layout.tsx
import { headers } from "next/headers";
import { detectDevice } from "react-device-detector/server";
import { DeviceProvider } from "react-device-detector";

const ua = (await headers()).get("user-agent") ?? "";
const device = detectDevice({ userAgent: ua, maxTouchPoints: 0 });

<DeviceProvider value={device}>{children}</DeviceProvider>

DeviceInfo is a plain object of booleans, so it serializes cleanly across the Server/Client boundary.

The detection gap, stated plainly

A component cannot know the device during server rendering.

There is no device to inspect, only an HTTP request. Views therefore render null until detection completes — which is precisely why v1’s flash of wrong-device content is gone. Two ways to handle that window:

<DesktopView renderWhileDetecting>…</DesktopView>  // render during detection
setServerDevice(detectDevice({ userAgent, maxTouchPoints: 0 }));  // or seed it

Header-based detection cannot see maxTouchPoints, so an iPadOS 13+ device looks like a Mac to the server. The client corrects it immediately after hydration. If iPad accuracy on the first paint matters, use <DesktopView renderWhileDetecting={false}> and let the client decide.

Accuracy

User-agent detection is a heuristic, not a fact. UA spoofing defeats it, so do browsers that freeze or reduce their UA string, and so do devices that do not exist yet.

Where a CSS media query (pointer: coarse, min-width) can answer your question, prefer it — a media query describes the actual viewport instead of guessing from a string.

This library is the right tool when you need to branch in JavaScript on device class and accept a heuristic’s error rate.

API

ExportTypeNotes
MobileView DesktopView IOSView AndroidView TabletView components accept renderWhileDetecting
useDevice() hook isMobile, isDesktop, isIOS, isAndroid, isTablet, isDetecting
detectDevice({ userAgent, maxTouchPoints }) function pure; usable on the server
setServerDevice(info) function seed the value for a correct first paint

Upgrading from v1

v1 carried two bugs serious enough to require a breaking change: every SSR page had a hydration mismatch, and modern iPads were detected as desktops. v2’s only peer dependency is react.

The full migration notes are in the README.