react-effectless
v1.0.9
Published
<div align="center"> <h1>🪝 react-effectless</h1>
Downloads
999
Maintainers
Readme
Install
npm install react-effectlessRequires React 16.8+.
Quick start
import {
useOnMount,
useEventSubscription,
useDebounce,
useInterval,
useTimeout,
} from 'react-effectless'Hooks
import { useOnMount } from 'react-effectless'
function Modal({ onOpen }) {
useOnMount(() => {
onOpen()
return () => console.log('unmounted')
})
}Replaces useEffect(fn, []). Makes intent explicit and handles StrictMode-safe cleanup.
import { useEventSubscription } from 'react-effectless'
function KeyLogger() {
const [key, setKey] = useState('')
useEventSubscription({
target: window,
event: 'keydown',
handler: (e) => setKey(e.key),
})
return <p>Last key: {key}</p>
}Without this hook, an inline handler reference changes every render; the listener is removed and re-added on every render. useEventSubscription stabilizes the handler ref internally.
import { useDebounce } from 'react-effectless'
function Search({ query }) {
const debouncedQuery = useDebounce({ value: query, delay: 300 })
// pass debouncedQuery to your data-fetching hook
}Rolling your own with useEffect + setTimeout misses clearTimeout on rapid changes, causing stale results to flash in.
import { useInterval } from 'react-effectless'
function Clock() {
const [time, setTime] = useState(new Date())
useInterval({
callback: () => setTime(new Date()),
delay: 1000,
})
return <p>{time.toLocaleTimeString()}</p>
}The classic footgun from Dan Abramov's post: a raw useEffect + setInterval captures the initial value of callback in a stale closure. useInterval always calls the latest version.
import { useTimeout } from 'react-effectless'
function Toast({ onDismiss }) {
useTimeout({ callback: onDismiss, delay: 3000 })
return <div className="toast">Saved!</div>
}Same stale-closure bug as useInterval, plus clearTimeout is easy to forget. Set delay to null to cancel.
Hook signatures
useOnMount(cb: () => void | (() => void)): void
useEventSubscription({ target, event, handler, options? }): void
useDebounce<T>({ value: T; delay: number }): T
useInterval({ callback: () => void; delay: number | null }): void
useTimeout({ callback: () => void; delay: number | null }): voidWhen not to use these hooks
For data fetching, use TanStack Query or RTK Query — they handle caching, race conditions, and loading state correctly.
For external store subscriptions, use useSyncExternalStore (built into React 18+).
For derived or computed values, use an inline const or useMemo — no hook needed.
ESLint plugin
Pair this library with eslint-plugin-react-effectless to catch useEffect anti-patterns at lint time.
Agent instructions
AI coding agents reach for useEffect by default. Run the bootstrapper once to inject a useEffect policy into every agent instruction file in your project:
npx react-effectless initIt writes or appends to:
| File | Agent |
| ------------------------------------------------------- | --------------------------------------------- |
| CLAUDE.md | Claude Code |
| AGENTS.md | OpenAI Codex |
| .cursor/rules/react-effectless.md | Cursor |
| .github/copilot-instructions.md | GitHub Copilot |
| .github/instructions/react-effectless.instructions.md | GitHub Copilot (scoped to **/*.ts,**/*.tsx) |
For files that already exist the policy is appended — your existing instructions are preserved. Re-running is safe: it detects the <!-- react-effectless --> marker and skips any file that already contains it.
Manual setup
# Claude Code
cat node_modules/react-effectless/agent-skills/CLAUDE.md >> CLAUDE.md
# Cursor
cp node_modules/react-effectless/agent-skills/cursor-rules.md .cursor/rules/react-effectless.mdThe raw templates live in agent-skills/ if you want to inspect or customize them.
