- MUST: Use @antfu/ni. Use
nito install,nr SCRIPT_NAMEto run.nunto uninstall. - MUST: Use TypeScript interfaces over types.
- MUST: Keep all types in the global scope.
- MUST: Use arrow functions over function declarations
- MUST: Never comment unless absolutely necessary.
- If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack
- Do not delete descriptive comments >3 lines without confirming with the user
- MUST: Use kebab-case for files
- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).
- Example: for .map(), you can use
innerXinstead ofx - Example: instead of
movedusedidPositionChange
- Example: for .map(), you can use
- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
- MUST: Do not type cast ("as") unless absolutely necessary
- MUST: Remove unused code and don't repeat yourself.
- MUST: Always search the codebase, think of many solutions, then implement the most elegant solution.
- MUST: Put all magic numbers in
constants.tsusingSCREAMING_SNAKE_CASEwith unit suffixes (_MS,_PX). - MUST: Put small, focused utility functions in
utils/with one utility per file. - MUST: Use Boolean over !!.
- MUST: Treat components as setup functions that run ONCE, not render functions.
- MUST: Place reactive work in primitives (
createMemo,createEffect,<Show>,<For>), not component body. - MUST: Access signals only inside reactive contexts (JSX expressions, effects, memos).
- MUST: Call signals as functions:
count()notcount. - MUST: Use functional updates when new state depends on old:
setCount((prev) => prev + 1). - MUST: Keep signals atomic (one per value) — one big state object loses granularity.
- MUST: Use derived functions
() => count() * 2for cheap/infrequent derivations. - MUST: Use
createMemo(() => ...)for expensive/frequent derivations — caches result. - MUST: Use
createEffectfor side effects only (DOM, localStorage, subscriptions). - MUST: Call
onCleanup(() => ...)inside effects for subscriptions/intervals/listeners. - MUST: Use path syntax for store updates:
setStore("users", 0, "name", "Jane"). - MUST: Wrap store props in arrow for
on():on(() => store.value, fn)noton(store.value, fn). - SHOULD: Use
{ equals: false }for trigger signals that always notify. - SHOULD: Use
batch(() => { ... })when updating multiple signals outside event handlers. - SHOULD: Use
on(dep, fn)for explicit effect dependencies. - SHOULD: Use
untrack(() => value())to read without subscribing. - SHOULD: Use
createStore({ ... })for nested objects with fine-grained reactivity. - SHOULD: Use
produce(draft => { ... })for complex store mutations. - NEVER: Derive state via
createEffect(() => setX(y()))— use memo or derived function. - NEVER: Place side effects inside
createMemo— causes infinite loops/crashes.
- MUST: Access props via
props.title, not destructuring. - SHOULD: Wrap in getter if needed:
const title = () => props.title. - SHOULD: Use
splitProps(props, ["keys"])to separate local from pass-through props. - SHOULD: Use
mergeProps(defaults, props)for default values. - SHOULD: Use
children(() => props.children)only when transforming, otherwise{props.children}. - NEVER: Destructure props
({ title })— breaks reactivity.
- MUST: Use
<For each={items()}>for object arrays — item is value, index is signal. - MUST: Use
<Index each={items()}>for primitives/inputs — item is signal, index is number. - MUST: Use
<Suspense fallback={...}>for async, not<Show when={!loading}>. - MUST: Access resource states via
data(),data.loading,data.error,data.latest. - SHOULD: Use
<Show when={cond()} fallback={...}>for conditionals. - SHOULD: Use
<Show when={val}>callback for type narrowing:{(v) => <div>{v().name}</div>}. - SHOULD: Use
<Switch>/<Match>for multiple conditions. - SHOULD: Use
createResource(source, fetcher)for reactive async data. - SHOULD: Use
<ErrorBoundary fallback={(err, reset) => ...}>for render errors. - NEVER: Use
.map()in JSX — use<For>or<Index>. - NEVER: Rely on ErrorBoundary for event handler or setTimeout errors — use try/catch.
- MUST: Use
classnotclassName. - MUST: Combine static
class="btn"with reactiveclassList={{ active: isActive() }}. - MUST: Use
onClickfor delegated events;on:clickfor native (element-level). - MUST: Condition inside handler since events are not reactive:
onClick={() => props.onClick?.()}. - MUST: Read refs in
onMountor effects — refs connect after render. - MUST: Call
onCleanupinside directives for cleanup. - SHOULD: Use
on:clickforstopPropagation, capture, passive, or custom events. - SHOULD: Use
style={{ color: color(), "--css-var": value() }}for inline styles. - SHOULD: Type refs as
let el: HTMLElement | undefinedwith guard. - SHOULD: Use
use:directiveName={accessor}for reusable DOM behaviors. - NEVER: Mix reactive
class={x()}withclassList.
Run dev packages/cli with:
npm_command=exec node packages/cli/dist/cli.jsRun checks always before committing with:
pnpm test # runs e2e tests
pnpm lint
pnpm typecheck # runs type checking
pnpm formatThis is a pnpm + Turborepo monorepo (19 packages under packages/). No external services (databases, Docker, etc.) are required.
pnpm build must complete before pnpm test or pnpm lint — Turborepo dependsOn enforces this, but be aware that pnpm test will rebuild if the build cache is cold. After modifying source files, always rebuild before running tests.
The root package.json has pnpm.onlyBuiltDependencies configured for @parcel/watcher, esbuild, sharp, spawn-sync, and unrs-resolver. Without this, pnpm install silently skips their native builds and downstream packages may fail.
E2E tests (pnpm test at root) run Playwright against the e2e-playground Vite dev server on port 5175 (auto-started by the Playwright config). Chromium must be installed: npx --prefix packages/react-grab playwright install chromium --with-deps.
See root package.json scripts and CONTRIBUTING.md for the full list. Quick reference:
- Install:
ni(orpnpm install) - Build:
nr build(orpnpm build) - Dev watch:
nr dev(orpnpm dev) — watches core packages - Test:
pnpm test— runs Playwright E2E + Vitest CLI tests - Lint:
pnpm lint— oxlint on react-grab package - Typecheck:
pnpm typecheck— tsc on react-grab package - Format:
pnpm format— oxfmt - CLI dev:
npm_command=exec node packages/cli/dist/cli.js - E2E playground:
pnpm --filter @react-grab/e2e-playground dev(port 5175)