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.