React utility hooks/components library. Monorepo with two packages:
react-simplikit(packages/core) — Platform-independent React hooks & components@react-simplikit/mobile(packages/mobile) — Mobile web utilities (viewport, keyboard, layout)
yarn build # Build all packages (tsup)
yarn test # Run tests (Vitest)
yarn fix # Auto-fix lint + format (ESLint + Prettier)
yarn typecheck # Type check (tsc --noEmit) - alias: yarn run test:type
yarn changeset # Create a changeset
yarn changeset status # Check pending changesetsLayer dependency is unidirectional — no upward or circular imports allowed:
components → hooks → utils → _internal
- Components may use hooks, utils, _internal
- Hooks may use utils, _internal
- Utils may use _internal only
- _internal has no internal dependencies
- Mobile may depend on core; core must NOT depend on mobile
Each hook/component/util lives in its own folder with co-located docs:
src/
├── hooks/
│ └── useHookName/
│ ├── index.ts # Re-export
│ ├── useHookName.ts # Implementation
│ ├── useHookName.spec.ts # Tests (core)
│ ├── useHookName.test.ts # Tests (mobile)
│ ├── useHookName.ssr.test.ts # SSR safety tests
│ ├── useHookName.md # English docs
│ └── ko/
│ └── useHookName.md # Korean docs
├── utils/
│ └── utilName/
│ ├── index.ts
│ ├── utilName.ts
│ └── utilName.test.ts
└── index.ts # Public API exports (alphabetically sorted)
typeoverinterface— Always usetypefor type aliases- Named functions in useEffect — Improves stack traces and readability
useEffect(function handleResize() { ... }, []); // ✅ useEffect(() => { ... }, []); // ❌
- Strict boolean checks — Use explicit comparisons (
value !== undefined, notif (value)) - Import extensions — Include
.jsin relative imports for ESM compliance - useEffect cleanup — Always return cleanup to remove listeners/subscriptions
"use client"banner — tsup adds this for RSC compatibility- Named exports only — No default exports
- No
anytypes — Full TypeScript strict mode, no escape hatches - Zero dependencies — No runtime dependencies in production code
All hooks/utils accessing browser APIs must be SSR-safe:
// Pattern: Fixed initial value + useEffect sync
const [state, setState] = useState(FIXED_INITIAL_VALUE);
useEffect(function syncBrowserState() {
if (isServer()) return;
setState(getBrowserAPI());
}, []);Never initialize state with browser API calls (causes hydration mismatch).
- Single value:
useDebounce<T>(value, delay): T - Tuple (state + action, 2 items):
useToggle(init): [boolean, () => void] - Object (3+ items):
usePagination(): { page, nextPage, prevPage }
- Throttle subscriptions at ~16ms (60fps) for viewport/keyboard events
- Deduplicate to skip updates when value hasn't changed
startTransitionfor non-urgent state updates (React 18+)
- 100% coverage mandatory — Enforced by Vitest coverage threshold, no exceptions
- SSR tests required — All hooks accessing browser APIs must have
.ssr.test.ts - Core tests:
.spec.ts(legacy, will migrate to.test.ts) - Mobile tests:
.test.ts - SSR pattern:
import { renderHookSSR } from '../utils/renderHookSSR'; it('is safe on server side rendering', () => { const result = renderHookSSR.serverOnly(() => useHookName()); expect(result.current).toBeDefined(); });
- Bilingual: English + Korean (co-located in hook folders as
*.md/ko/*.md) - JSDoc required: Every public API must have
@description+@example+@param+@returns - VitePress: Used for documentation site; rewrites map source docs to clean URLs
- Co-location: Docs live inside package source, not in separate docs/ tree
- Homepage:
https://react-simplikit.slash.page
Format: <type>(<scope>): <description>
- Types:
feat,fix,docs,chore,refactor,test - Scope: Package name (
core,mobile) or area - Examples:
feat(mobile): add useKeyboardHeight hook,fix: correct SSR rendering logic
- Tests pass (
yarn test) - Lint/format pass (
yarn fix) - 100% coverage maintained
- JSDoc with
@description+@example - Changeset added (if user-facing change)
Uses Changesets + GitHub Actions OIDC for automated releases.
PR with changeset merged → release.yml triggers
→ changesets/action detects changeset → creates "Version PR"
→ Version PR merged → changesets/action: hasChangesets=false
→ "Publish to npm" step → changeset publish (uses npm publish internally)
npm does NOT support publishConfig overrides for manifest fields (main, types, module, exports). Only access, registry, tag are supported. See npm/cli#7586.
yarn npm publishDOES support publishConfig field overrideschangeset publishinternally callsnpm publish(not yarn)- Therefore: always declare
main/types/module/exportsat the top level of package.json publishConfigshould only containaccess: "public"
- npm auth uses GitHub Actions OIDC (no secret tokens needed)
- Requires
id-token: writepermission in workflow - Node 22 ships npm v10 which doesn't support OIDC →
npm install -g npm@latestupgrades to npm 11+ changesets/actionmust NOT havepublishoption (it overwrites.npmrcand breaks OIDC)- Publish is done in a separate step after changesets/action
GITHUB_TOKEN=$(gh auth token) yarn changeset version --snapshot canary
yarn changeset publish --tag canary # Requires npm login + OTPpackages/
├── core/ # react-simplikit
│ ├── src/ # Source (hooks, components, utils)
│ ├── dist/ # CJS build output
│ ├── esm/ # ESM build output
│ └── package.json
└── mobile/ # @react-simplikit/mobile
├── src/
├── dist/ # CJS + ESM build output
└── package.json
{ "main": "./dist/index.cjs", // CJS entry (top level, NOT in publishConfig) "module": "./esm/index.js", // ESM entry for bundlers "types": "./dist/index.d.cts", // TypeScript types "exports": { // Modern Node.js/bundler resolution ".": { "import": { "types": "...", "default": "..." }, "require": { "types": "...", "default": "..." }, }, }, "publishConfig": { "access": "public", // ONLY access here, nothing else }, }