new-hook — scaffold a hook the ReactUse way
Use this when adding a new hook to @reactuses/core. It encodes how hooks are actually
built in this repo today. Pair it with hook-test and hook-docs afterward.
There is no scaffolding script — hooks are created by hand. Follow the steps below.
Step 0 — Understand before writing (don't skip)
- Confirm the hook doesn't already exist:
ls packages/core/src/useX. - Read 1-2 similar existing hooks end to end (not just their names) to match style —
e.g. a state hook like
useCounter, a browser hook likeuseClipboard, an element hook likeuseEventListener. Note their return shape and which utils they reuse. - Check the knowledge base reuse cheat-sheet — much of what you
need (
useLatest,useUnmount,useEventListener,defaultWindow,isBrowser) is already written. Reuse before writing.
Step 1 — Create the hook folder
packages/core/src/useX/ with two (then three) files:
index.ts— the implementation, named exportexport const useX: UseX = ….interface.ts— the public typeUseXwith multilingual JSDoc.index.spec.ts— tests (hand off to the hook-test skill).
interface.ts template
Types live here (not inline), so the API-doc generator can read them. JSDoc is trilingual:
/**
* @title useX
* @returns_en What the hook returns, described for docs.
* @returns 返回值说明(简体)。
* @returns_zh-Hant 返回值說明(繁體)。
*/
export type UseX = (
/**
* @en The first argument, described.
* @zh 第一个参数说明。
* @zh-Hant 第一個參數說明。
* @defaultValue 0
*/
initial?: number,
) => readonly [number, (n: number) => void]
(See packages/core/src/useCounter/interface.ts for a full real example.)
index.ts template
import { useState, useCallback } from 'react'
import { isDev, isFunction } from '../utils/is'
import type { UseX } from './interface'
export const useX: UseX = (initial = 0) => {
if (isDev && initial != null && !isFunction(initial) && typeof initial !== 'number') {
console.error(`useX: \`initial\` expected number, got "${typeof initial}".`)
}
const [value, setValue] = useState(initial)
const set = useCallback((n: number) => setValue(n), [])
return [value, set] as const
}
Step 2 — Wire it into the public API
Edit packages/core/src/index.ts — all three parts, or the hook/types won't ship:
import { useX } from './useX' // 1. with the other imports
export {
// …
useX, // 2. inside the export { } block
}
export * from './useX/interface' // 3. with the other `export *` lines
Conventions to follow
- SSR-safety is mandatory. Guard any
window/document/navigatoraccess withdefaultWindow/defaultDocument(../utils/browser) orisBrowser/isNavigator(../utils/is). For subscribed external state, use theuse-sync-external-store/shimpattern with a server fallback (seeuseColorMode,useLocationSelector). - Reuse utilities — don't hand-roll
addEventListener(useuseEventListener), stale closures (useLatest), or unmount cleanup (useUnmount). - Return shape: a tuple with
as const, or an explicitly typed object. Be consistent with the closest existing hook. - Dev-time validation of arguments behind
if (isDev) { … console.error(…) }. - Stay on task — don't refactor neighbors or fix unrelated lint while you're here.
Step 3 — Then
- Tests → hook-test skill.
- Docs → hook-docs skill (+
bash scripts/generate-hook-registry.sh). - Verify:
pnpm --filter @reactuses/core test useXandpnpm lint.
Or run /new-hook <useName> "<description>" <category> to do the whole pipeline at once.