A library of general-purpose React hooks, published to npm as ESM-only and consumed by React 16.8+ applications in
browsers and during SSR. Every hook is public API: a signature change is a change to thousands of downstream builds, and
merging to master publishes automatically. Correctness under SSR and a small, honest public surface matter more than
feature count.
Language-level conventions (TypeScript, React, testing) live in .claude/rules/ and load per file type. This file
covers what is specific to this repository.
src/
├── index.ts # barrel; every hook and public type is re-exported here
├── types.ts # shared public types
├── <useHook>/
│ ├── index.ts # implementation -- named export, explicit return type, JSDoc
│ ├── index.dom.test.ts # DOM test (vitest "DOM" project, jsdom)
│ └── index.ssr.test.ts # SSR test (vitest "SSR" project, node)
└── util/ # internals: const.ts (isBrowser), misc.ts, resolve-hook-state.ts
└── testing/ # test-only helpers, excluded from the build
dist/ # tsc output, published as-is -- never edit
vite.config.ts # test, lint and format config, all three
- Vite+ is the toolchain:
vp fmt,vp lint,vp test. All three configured invite.config.ts, nowhere else. - oxlint with
@ver0/oxlint-configpresets plusoxlint-tsgolinttype-aware rules; oxfmt for formatting. - TypeScript 7 native compiler builds
distfromtsconfig.build.json. No bundler. - Vitest 4, two projects split by filename:
*.dom.test.tsruns in jsdom,*.ssr.test.tsin node. - yarn 4 via corepack, for dependency work only (
yarn up <pkg>,yarn install --immutable). - semantic-release publishes from
master; GitHub Actions are SHA-pinned viapinact. - CI goes through
vponly:voidzero-dev/setup-vpprovides Node, yarn, the dependency cache and the install, so nocorepack enable, noactions/setup-nodeand no directyarncall belongs in a workflow.
Hooks:
- One directory per hook,
src/<useHook>/index.ts, named export, no default export. - Export every custom parameter and return type alongside the hook. No
I/Tprefixes. - Re-export from
src/index.tsunder its category comment, and add an entry to the README hook list. - Import sibling hooks by path with extension:
import {useToggle} from '../useToggle/index.js', never from'..'. - Guard browser APIs with
isBrowserfrom../util/const.js. A hook must not throw when rendered on the server. - No fallbacks for legacy browser APIs — vendor prefixes,
addListener/removeListener, existence guards on standard methods were removed deliberately. Target current browsers. - Dev-only warnings go behind
process.env.NODE_ENV === 'development'.
Types:
isolatedDeclarationsis on for the build: every exported symbol needs an explicit type annotation, includingexport const.@types/nodeis out of scope (types: []); onlyprocess.env.NODE_ENVis declared, insrc/util/process-env.d.ts.noUncheckedIndexedAccessis on. An indexed read that cannot beundefinedneeds a!plus a comment saying why.
Tests:
- Every hook needs both files; SSR tests cover at least
'should be defined'and'should render'. - Assertions on hook output go through the helpers in
src/util/testing/test-helpers.js—expectResultValue,expectCallArgs,expectCallResult— which assert presence instead of asserting types away. - Aim for full coverage; where a branch is unreachable, leave a comment explaining why.
Lint config:
- Rule exceptions belong in the
repoOverridesobject invite.config.ts, which must stay the last entry oflint.extends: consumer-levelrules/overridesmerge before extended configs, so anything placed in thelintblock's ownrulesloses to the presets. - A test-file override must redeclare
plugins: ['vitest']or its vitest rules are silently ignored.
Commits:
- Conventional subjects drive releases:
featminor,fix/perfpatch,chore/ci/docs/test/styleno release. - A public API break needs
!in the subject and aBREAKING CHANGE:footer — the footer is what semantic-release reads.
vp fmt --write— oxfmt;vp fmt --checkis what CI runs.vp lint— oxlint plus full tsc diagnostics (lint.options.typeCheck), so this also type-checks test files, whichtsconfig.build.jsonexcludes.vp test --run— full suite;vp test --run --coveragefor coverage.tsc --project ./tsconfig.build.json— the build;isolatedDeclarationsviolations surface only here.
CI runs Lint, Build and Test (Node LTS) on every PR and master push.
- A
feat,fixor breaking commit reachingmasteris published to npm within minutes. There is no staging step — land release-worthy work through a PR and merge deliberately. - Never edit
dist/. It is regenerated by every build. - Changes to
exports,filesor a hook's signature alter the published contract. Verify by packing (npm pack) and resolving the specifiers from a scratch consumer, not by readingdist. - A hook missing from
src/index.tsor from the README list ships invisible — the README list is the documentation.