Imported from Valinova/developer-config (
always/skills/no-use-effect/SKILL.md). Install upstream withnpx skills add Valinova/developer-config --skill no-use-effect. Copyright stays with the author.
No useEffect
Never call useEffect directly without first exhausting the repository's preferred alternatives. Check project documentation for a stricter local policy.
Instead of useEffect for... |
Use |
|---|---|
| Deriving state from state or props | Inline computation or useMemo |
| Fetching data | Server actions, useQuery, loaders, or the project's data layer |
| Responding to user actions | Event handlers |
| One-time external synchronization | A dedicated mount hook when the project provides one |
| Resetting state when identity changes | A key prop on the parent |
Decision Process
- If the effect derives state, compute the value during render or with
useMemo. - If it fetches data, use the project's data-fetching pattern.
- If it responds to a user action through a flag, move the work into the event handler.
- If it synchronizes once with an external system, use the project's mount or subscription abstraction.
- If it resets state when an ID or key changes, force a remount with the parent's
key. - If none apply, treat it as a possible genuine effect and document why it must synchronize with an external system.
Common Patterns
Derive values directly instead of synchronizing store-derived state:
// Avoid
const items = useStore((state) => state.items)
useEffect(() => setActive(items[0]?.id), [items])
// Prefer
const items = useStore((state) => state.items)
const active =
selectedId && items.some((item) => item.id === selectedId)
? selectedId
: items[0]?.id
Use the project's editable-field abstraction instead of focus-guarded effects:
// Avoid
useEffect(() => {
if (!focused) setLocal(storeValue)
}, [storeValue, focused])
// Prefer
const { displayValue, handlers } = useEditableField(storeValue, onChange)
After refactoring, run type checking and linting and confirm that observable component behavior is unchanged.