Create Custom Hook
Create a custom React hook following Darkroom conventions. The hook itself is stack-agnostic — what differs between satus and novus is the path alias.
Step 1 — Detect stack
Read package.json:
dependencies.next→ satus / Next.js (path alias@/, hooks require a'use client'boundary)dependencies["react-router"]→ novus / React Router (path alias~/, components isomorphic)
Step 2 — Choose location
| Stack | Hook path |
|---|---|
| satus | lib/hooks/<name>.ts |
| novus | hooks/<name>.ts (novus puts hooks/ at the project root) |
Confirm by checking the existing lib/hooks/ or hooks/ directory structure if either pattern is unclear from package.json alone.
Step 3 — Emit template
satus / Next.js
// lib/hooks/<name>.ts
'use client'
// Client Components can prerender on the server. Read browser APIs in effects
// or event handlers, with the same initial state on the server and browser.
import { useState, useEffect } from 'react'
interface Use<Name>Options {
// Hook configuration options
}
interface Use<Name>Return {
// Return type definition
}
export function use<Name>(options?: Use<Name>Options): Use<Name>Return {
// Implementation
return {
// Return values
}
}
novus / React Router
// hooks/<name>.ts
// No 'use client' — RR components are isomorphic; the hook runs wherever
// it's called from. Read browser APIs in effects or event handlers, with the
// same initial state on the server and browser.
import { useState, useEffect } from 'react'
interface Use<Name>Options {
// Hook configuration options
}
interface Use<Name>Return {
// Return type definition
}
export function use<Name>(options?: Use<Name>Options): Use<Name>Return {
// Implementation
return {
// Return values
}
}
Conventions (both stacks)
- Type everything — options interface, return interface.
- Named export —
export function useX, not default. - Prefix with
use— React hook naming convention. - No memoization — React Compiler handles it automatically.
- Hydration-safe state — both stacks can render on the server. Use a
deterministic initial value for the server render and first browser render;
'use client'does not disable prerendering. Atypeof windowbranch in a lazy state initializer can still produce different markup and a hydration mismatch. ReadlocalStoragein an effect, and finish that read before enabling an effect that persists state, so initial defaults cannot overwrite stored values.
Stack-specific
| satus | novus | |
|---|---|---|
| Directive | 'use client' (hooks live in client boundary) |
None (isomorphic) |
| Browser APIs | SSR-safe initial state; effects or event handlers | SSR-safe initial state; effects or event handlers |
| Path alias | @/ |
~/ |
Before you start
If this hook uses an external library, fetch docs first:
- Use Context7 MCP (
mcp__context7__resolve-library-id→get-library-docs) in Claude or when the user configured it in standalone Codex. Otherwise use official docs through native browsing or inspect the pinned local package. cc-settings does not auto-run unpinned registry MCP packages in Codex. - Run
bun info <package>to check the latest version.
Consider Using Hamo
For common use cases, prefer hamo hooks (fetch hamo docs via Context7 first):
import { useWindowSize, useRect, useIntersectionObserver } from 'hamo'
Only create custom hooks when hamo doesn't cover the use case.
Example
User: "create a useLocalStorage hook" (in satus repo)
→ Creates lib/hooks/use-local-storage.ts with 'use client', deterministic initial state, effect-based storage read before writes
User: "create a useLocalStorage hook" (in novus repo)
→ Creates hooks/use-local-storage.ts, no directive, deterministic initial state, effect-based storage read before writes
Arguments
$ARGUMENTS— Hook name (e.g., "useAuth", "useLocalStorage")