react-device-detector

React components for device-aware rendering. Version 2 fixes server hydration mismatches, modern iPad detection and React 19 compatibility.

React
TypeScript
SSR
npm
react-device-detector

What it does

Render different components per device class, without writing the detection yourself:

<MobileView>shown on phones and tablets</MobileView> <DesktopView>shown on desktop</DesktopView> <TabletView>shown on tablets</TabletView>

Zero runtime dependencies, TypeScript types included, works with React 17 through 19.

The two bugs v2 fixed

Both had been shipping since 2021, and both were invisible in ordinary use.

Every server-rendered page had a hydration mismatch

v1 computed its flags once, at module load:

export const isIOS = getOS() === "iOS"; // evaluated at import time

On the server there is no window, so every flag resolved to desktop. The server therefore sent desktop markup to everyone — phone users included — and the client then computed something different, producing a React hydration mismatch on every SSR page. The values were also frozen for the lifetime of the process and could never change.

v2 detects inside a hook, so the server render and the first client render agree by construction. There's a test that renders through real react-dom/server and asserts the output is empty — proving the fix rather than asserting it.

Every modern iPad was classified as a desktop

iPadOS 13+ ships a desktop-class Safari that identifies as Macintosh. The original /iPad|iPhone|iPod/ test never matched it:

/iPad|iPhone|iPod/.test(iPadOS17UserAgent); // false

v2 disambiguates with navigator.maxTouchPoints — a Mac reports 0, an iPad reports 5 — with tests in both directions, so a real Mac isn't misread as an iPad either.

Packaging

v1 declared react-scripts, web-vitals and three @testing-library packages as peer dependencies. That was Create React App scaffolding that leaked into the published manifest, and every consumer was asked to install it. v2's only peer is react, widened to ^17 || ^18 || ^19.

It also dropped @babel/runtime, which finally makes the readme's long-standing "no dependency" claim true.

What I'd highlight

The two bugs share one cause: the library was scaffolded with Create React App, so CRA's dependencies landed in peerDependencies and CRA's client-only assumptions landed in the detection code. The fix wasn't only to correct them but to move the demo app into example/, so it structurally cannot leak into the package again.

I removed the isMobile / isIOS constants rather than deprecating them. They cannot be made SSR-safe — their value is decided before React renders anything. Keeping them would have preserved the exact bug behind a compatible-looking name, which is worse than a documented breaking change.

Migrating to v2

The breaking change is narrow but intentional. Applications should move render-time checks into the view components or the detection hook instead of importing a frozen module constant. That keeps detection inside React's lifecycle, where server rendering and hydration can agree.

Pages that need deterministic server output can also pass an explicit user agent from their request boundary. The important part is ownership: request-specific data belongs to the request, not to a process-wide module that may serve many visitors.

The package keeps the component API small so migration can happen one surface at a time. A team can replace its highest-risk conditional views first, verify the server markup, and then remove the legacy constants.

Detection is not responsive design

Device detection should not decide whether a two-column layout collapses or whether text fits a viewport. CSS media and container queries are better at presentation because they respond to the space actually available.

The library fits behavioral differences: loading a genuinely device-specific interaction, choosing an input pattern, or isolating code that depends on a capability unavailable to another class of device. Even there, feature detection is preferable when the browser exposes a reliable signal.

That boundary keeps user-agent heuristics in their proper place. They are useful evidence, not a complete description of a device.

Release discipline

The release matrix covers React 17, 18, and 19, server rendering, modern iPad user agents, real Mac user agents, and the packed npm artifact. Testing the tarball matters because a source tree can be healthy while its exports, type declarations, or peer dependencies are wrong after publication.

The example app remains separate from the package and serves as a consumer rather than a privileged internal build. If the example can install and render the packed output, it exercises the same boundary downstream applications do.