react-dead-code-canary
v0.1.0
Published
Zero-dependency React canary that flags unused UI modules in production by tracking mounts, visibility, and interactions locally.
Maintainers
Readme
react-dead-code-canary
Zero-dependency React utility that flags unused UI modules in production. Wrap a suspicious
component in <Canary>, ship it, and let real users prove whether it is ever seen or touched.
Everything is recorded locally in the visitor's browser; you decide if/where telemetry is sent.
Features
- Drop-in wrapper —
<Canary id="...">renders children through adisplay: contentsdiv, so layout is untouched. - Three signals per component — mounts (
mountCount,firstMountedAt,lastMountedAt), visibility viaIntersectionObserver(lastSeenAt), and real usage via capture-phaseclick/focusin/changelisteners (lastInteractedAt). - Structured reports —
useCanaryReport()aggregates every record into a JSON-friendlyCanaryReportwith a computeddeadComponentslist. - Bring-your-own telemetry —
onReportSendfires a debounced report once per session per id;flushCanaryReport(sender)sends on demand. Nothing is transmitted unless you wire it up. - Safe everywhere — SSR-safe (
"use client"banner, every browser API guarded), localStorage wrapped in try/catch with an in-memory fallback, zero runtime dependencies, tree-shakeable (sideEffects: false), ESM + CJS + types.
Install
npm install react-dead-code-canary
# peer deps: react@^18||^19, react-dom@^18||^19Quickstart
"use client"; // already applied inside the package; shown here for clarity in Next.js apps
import { Canary, useCanaryReport, flushCanaryReport, clearCanaryData } from "react-dead-code-canary";
// 1. Wrap anything you suspect is dead weight.
export function Sidebar() {
return (
<Canary
id="legacy-export-panel"
thresholdDays={30}
onReportSend={(report) =>
fetch("/api/dead-code", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(report),
})
}
>
<LegacyExportPanel />
</Canary>
);
}
// 2. Read the aggregated report anywhere (e.g. an internal audit page).
export function AuditPage() {
const { entries, deadComponents, refresh } = useCanaryReport({ thresholdDays: 30 });
return (
<>
<button onClick={refresh}>Refresh</button>
<button onClick={() => flushCanaryReport((r) => console.log(r))}>Send now</button>
<button onClick={clearCanaryData}>Reset</button>
<pre>{JSON.stringify(deadComponents, null, 2)}</pre>
<p>{entries.length} tracked components</p>
</>
);
}A full interactive demo lives in example/DeadCodeAuditDashboard.tsx
(run steps in PUBLISH-AND-TEST.md).
API
<Canary> props
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| id | string | yes | Stable, globally unique id for the wrapped module. Used as the storage key (rdcc:v1:<id>). |
| thresholdDays | number | yes | Days of inactivity before this component counts as dead in reports sent via onReportSend. |
| onReportSend | (report: CanaryReport) => void \| Promise<void> | no | Receives the aggregated report, debounced ~3s after mount, at most once per JS session per id. Failures are logged, never thrown. |
| disabled | boolean | no | Render children untouched and record nothing (default false). |
| children | ReactNode | yes | The UI module under observation. |
Exports
| Export | Kind | Description |
| --- | --- | --- |
| Canary | component | Tracking wrapper (see props above). |
| useCanaryReport(options?) | hook | Returns CanaryReport & { refresh(): void }. Snapshot is computed on mount and when options.thresholdDays changes; call refresh() to re-read storage. |
| flushCanaryReport(sender, options?) | function | Builds a fresh report, awaits sender(report), returns the report. Logs and rethrows sender failures (no debounce / session guard). |
| clearCanaryData() | function | Deletes every rdcc:v1:* record and re-arms the once-per-session onReportSend guard. |
| buildCanaryReport(thresholdDays?) | function | Pure report builder used by the hook and senders. |
| isRecordDead(record, now, thresholdDays) | function | The dead-detection predicate (see rule below). |
| normalizeThresholdDays(value) | function | Clamps invalid thresholds to the default (30). |
| getCanaryStorage() / createLocalStorageStorage(store) / createInMemoryStorage() | functions | Storage backend access for advanced use/testing. |
| DEFAULT_THRESHOLD_DAYS, STORAGE_KEY_PREFIX | constants | 30 and "rdcc:v1:". |
| CanaryRecord, CanaryReport, CanaryStorage, CanaryReportSender, CanaryProps, StorageLike | types | Full TypeScript surface. |
Record shape
interface CanaryRecord {
id: string;
firstMountedAt: number; // epoch ms
lastMountedAt: number; // epoch ms
mountCount: number;
lastSeenAt: number | null; // IntersectionObserver
lastInteractedAt: number | null; // click / focusin / change
}The dead-detection rule (exact)
A component is classified DEAD when both conditions hold at report time:
now - max(lastInteractedAt ?? 0, firstMountedAt) >= thresholdDays * 86_400_000
AND lastInteractedAt === null // it has never been interacted withIn plain words: it has never received a click/focus/change, and at least thresholdDays have
passed since it first mounted. Consequences worth knowing:
- One single interaction — ever — permanently exempts a component from the dead list.
- Being seen (
lastSeenAt) does not rescue a component; visibility is reported for context only. - A freshly shipped canary is never flagged before
thresholdDayselapse, so new code gets a fair observation window. - The comparison is inclusive (
>=), andthresholdDaysmay be0(useful in demos/tests: every never-interacted component is flagged immediately).
Notes & caveats
- Per-browser data. Records live in each visitor's localStorage, so a component is only truly
dead org-wide when reports streamed from many sessions (via
onReportSend) agree. - React StrictMode double-mounts in development, which inflates
mountCountby 2 per load there. Production numbers are exact. - If
IntersectionObserveris missing, visibility tracking is skipped (lastSeenAtstaysnull); mounts and interactions still work. If localStorage is unavailable (SSR, privacy mode), an in-memory fallback keeps the API functional for the session. - The observer watches the element children present at mount; children appended much later are not re-observed.
Privacy
This package stores only component id strings and numeric timestamps, locally, in the
visitor's own browser (localStorage keys prefixed rdcc:v1:). No user input, no content, no
cookies, no fingerprinting, and no network calls of any kind unless you implement
onReportSend / flushCanaryReport — in which case the payload is exactly the CanaryReport
JSON documented above. clearCanaryData() deletes everything.
License
MIT © 2026 Dinesh Kumar
