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.
