feat: add useDevicePixelRatio - #17
Conversation
The DPR store needs the same Safari < 14 addListener fallback the device store already carries. Sharing one copy keeps the legacy branch in a single place.
Returns window.devicePixelRatio through the same SSR contract as the other hooks: server render and hydration first paint both report 1, and the real ratio arrives one render later. The ratio is reactive because it moves on browser zoom, on a display scale change, and when a window is dragged between screens of different densities. A resolution media query matches one exact ratio, so the listener re-arms on the new query every time the value moves. The store lives in its own module rather than in DeviceInfo, which keeps the useDevice budget flat: 609 B when imported alone.
Both examples render the ratio under data-testid="dpr", and the Next.js example captures the first-paint value the way it already captures the device snapshot. The expected ratio is read from the Playwright descriptor instead of being duplicated in the spec, so the matrix covers 1, 2, 2.5 and 3 and survives a descriptor change. The rotation test now also asserts the ratio holds, since it tracks display density rather than viewport size.
The hook gets an API reference section in both READMEs and a row in the website API table. It deliberately stays out of the hero copy, the "what this solves" section, and the feature cards: those carry the detection story, and reading a ratio involves no detection. Known limitations gain three entries: the server cannot know the ratio, browser zoom is indistinguishable from a denser screen, and Safari before 16 never updates it. The everything budget measured 1.63 kB after the hook landed, so the ~1.5 kB claim moves to ~1.6 kB everywhere it appears. Unit test count goes from 85 to 98, and CLAUDE.md's sync instruction now names the website files it was missing.
Applied to README.ko.md and website/content/ko.ts. Causal links now run through sentence structure instead of the connective adverbs the style guide rules out, and each edit leaves a short sentence next to a long one rather than a chain of even-length ones. Dropped the sentence-wide bold in the CSS-versus-hook bullet, which is prose rather than a heading, and restated its claim: a correction render keeps CLS at zero because it does not touch geometry, not merely because it happens after the fact. No structural change, so both READMEs still mirror each other line for line.
website/tsconfig.tsbuildinfo is a local typecheck artifact and does not belong in the tree.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds a new useDevicePixelRatio() hook to the library, backed by a dedicated reactive DPR store, so consumers can reactively read window.devicePixelRatio while preserving the existing SSR and bundle-size contracts.
Changes:
- Introduces
src/core/dpr.tsplususeDevicePixelRatio()hook/export, with SSR default of1and lazymatchMedia('(resolution: Xdppx)')subscription. - Extracts shared
matchMedialistener helpers intosrc/core/media.tsand updates existing stores to reuse them. - Updates unit/E2E tests and documentation/website copy to cover the new hook and updated size/test-count claims.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| website/tsconfig.tsbuildinfo | Adds a TypeScript build info artifact (should not be tracked). |
| website/lib/seo.ts | Updates site description size claim and adds DPR-related keyword. |
| website/content/en.ts | Updates hero badge/test-count copy and adds API row for useDevicePixelRatio(). |
| website/content/ko.ts | Updates hero badge/test-count copy and adds API row for useDevicePixelRatio(). |
| src/core/media.ts | New shared listen/unlisten helpers for matchMedia change events. |
| src/core/dpr.ts | New DPR reactive store with SSR default 1 and resolution-query listener. |
| src/useDevicePixelRatio.ts | New public hook using useSyncExternalStore (or fallback) wired to the DPR store. |
| src/index.ts | Exports useDevicePixelRatio from the public entrypoint. |
| src/core/store.ts | Switches to shared listen/unlisten helpers (removes local copies). |
| src/test/dpr.test.ts | New unit tests covering DPR store behavior and listener lifecycle. |
| src/test/helpers.ts | Adds stubDevicePixelRatio and dprQuery helpers for DPR tests. |
| src/test/hooks.test.tsx | Adds hook-level tests for useDevicePixelRatio() behavior and non-interference with useDevice(). |
| src/test/setup.ts | Resets DPR store between tests to avoid state leakage. |
| src/test/ssr.test.tsx | Adds SSR safety regression test for useDevicePixelRatio() returning 1 on server. |
| e2e/device-detection.spec.ts | Extends Playwright matrix to assert DPR in CSR/SSR flows using deviceScaleFactor. |
| examples/basic/App.tsx | Displays DPR in the CSR example and updates user hint text. |
| examples/nextjs/app/DeviceDemo.tsx | Displays live DPR and captures first-paint DPR (1) in the SSR example. |
| README.md | Documents useDevicePixelRatio(), updates bundle-size/test-count claims, and adds limitations. |
| README.ko.md | Korean docs updates mirroring README changes and adds DPR limitations. |
| package.json | Adds size-limit budget entry for { useDevicePixelRatio } and adds related keywords. |
| CLAUDE.md | Updates architecture/testing guidance and invariants to include the DPR store and hook. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Added
useDevicePixelRatio(), a hook returningwindow.devicePixelRatiofor@2x/@3xasset selection, canvas backing store scaling, and tile resolution requests.Reactive: the value moves on browser zoom, on a display scale change, and when the window crosses screens of different densities. Server render and hydration first paint both report
1, so server and client HTML always match, and the real ratio arrives one render later.The ratio has its own store instead of a field on
DeviceInfo, souseDevice()keeps exactly two listeners and its budget stays flat.{ useIsMobile }{ useDevice }{ detectDevice }{ useDevicePixelRatio }13 new unit tests (85 → 98). E2E asserts the ratio across all six device profiles, reading the expected value from the Playwright descriptor's
deviceScaleFactorso the matrix covers 1, 2, 2.5 and 3.Docs
resolutionmedia query support so the value never updates there.~1.5 kBclaim moves to~1.6 kBeverywhere it appears, and the unit test count to 98.README.ko.mdandwebsite/content/ko.ts: causal links now run through sentence structure rather than connective adverbs, and one sentence-wide bold came out. One claim was corrected on the way: a correction render keeps CLS at zero because it does not touch geometry, not merely because it lands after the fact.CLAUDE.mdgains the new modules and the invariant that the ratio stays out ofDeviceInfo. Its count-sync instruction now names the website files it was missing.Also
listen/unlisten(the Safari < 14addListenerfallback) moved fromcore/store.tstocore/media.ts, since both stores need them.*.tsbuildinfoadded to.gitignore.