react-device-detector

React components that render by device type. v2 fixed a hydration mismatch that had been silently sending desktop markup to phone users on every server-rendered page, and iPad detection that classified every modern iPad as a desktop.

React
TypeScript
SSR
npm

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.