diff --git a/skills/react-named-effects.zip b/skills/react-named-effects.zip new file mode 100644 index 00000000..6780c7cc Binary files /dev/null and b/skills/react-named-effects.zip differ diff --git a/skills/react-named-effects/AGENTS.md b/skills/react-named-effects/AGENTS.md new file mode 100644 index 00000000..477c1557 --- /dev/null +++ b/skills/react-named-effects/AGENTS.md @@ -0,0 +1,257 @@ +# Named Effects — Complete Guide + +**Version 1.0.0** +Community +April 2026 + +> **Note:** +> This document is the compiled version of the react-named-effects skill with all reference content expanded inline. It is generated from `SKILL.md`, `references/diagnostics.md`, and `references/conventions.md`. Edit the source files, not this document. For the progressive-disclosure version, start with `SKILL.md`. + +## Core Pattern + +Replace anonymous arrows in `useEffect` with named function expressions. The name documents intent, improves stack traces, and acts as a diagnostic — effects that resist clear naming have structural problems. + +> **Note:** React's official documentation uses anonymous arrows in all `useEffect` examples for brevity. This skill recommends named expressions for production code where readability and debugging outweigh conciseness — the same reason we name variables instead of using single letters. + +```tsx +// Anonymous — tells you nothing +useEffect(() => { + const ws = new WebSocket(`wss://api/${id}`); + ws.onmessage = (e) => setData(JSON.parse(e.data)); + return () => ws.close(); +}, [id]); + +// Named — intent is immediate +useEffect(function connectToWebSocket() { + const ws = new WebSocket(`wss://api/${id}`); + ws.onmessage = (e) => setData(JSON.parse(e.data)); + return () => ws.close(); +}, [id]); +``` + +## Why This Matters + +**Stack traces improve.** Anonymous effects show `at (anonymous) @ InventorySync.tsx:14`. Named effects show `at connectToWebSocket @ InventorySync.tsx:14`. This works in Chrome, Firefox, Node.js, Sentry, and Datadog — the name comes from the JavaScript engine, not React. + +**Component data flow becomes scannable.** Four named effects tell you everything without reading a single line of implementation: + +```tsx +useEffect(function connectToInventoryWebSocket() { /* ... */ }, [warehouseId]); +useEffect(function fetchInitialStock() { /* ... */ }, [warehouseId, locationId, connected]); +useEffect(function resetStockOnLocationChange() { /* ... */ }, [locationId]); +useEffect(function synchronizeDocumentTitle() { /* ... */ }, [stock]); +``` + +**Naming reveals structural problems.** If a name requires "and," the effect should be split. If the name sounds like state shuffling (`syncDerivedValue`, `updateStateFromProps`), the effect probably shouldn't exist. + +## When to Apply + +Name every effect by default. It costs nothing and the benefits compound. Especially valuable when: +- A component has 2 or more effects +- An effect body exceeds a few lines +- Reviewing or debugging unfamiliar code +- Triaging errors in monitoring tools (Sentry, Datadog) + +--- + +## Diagnostics: What Naming Reveals + +Naming effects isn't just style — it's a diagnostic tool. When an effect resists a clean name, the name is telling you the effect has a structural problem. + +### Split Signal: "And" in the Name + +If the best name contains "and," the effect handles two concerns: + +```tsx +// Name requires "and" — two unrelated concerns +useEffect(function syncWidthAndApplyTheme() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + if (user?.preferences?.theme) { + document.body.className = user.preferences.theme; + } + return () => window.removeEventListener('resize', handleResize); +}, [user?.preferences?.theme]); +``` + +```tsx +// Two focused effects +useEffect(function trackWindowWidth() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); +}, []); + +useEffect(function applyUserTheme() { + if (user?.preferences?.theme) { + document.body.className = user.preferences.theme; + } +}, [user?.preferences?.theme]); +``` + +After splitting, each effect has a single clear name and its own dependency array. Dependencies that seemed unrelated in the combined effect now make sense in isolation. + +### Elimination Signal: State Shuffling Names + +If the best name sounds like internal state management, the effect likely shouldn't exist: + +```tsx +// Vague name — signals derived state stored in state +useEffect(function syncFullName() { + setFullName(`${firstName} ${lastName}`); +}, [firstName, lastName]); + +// Derive during render instead — no effect, no extra render cycle +const fullName = `${firstName} ${lastName}`; +``` + +The effect version triggers an extra render: React renders, runs the effect, calls `setFullName`, re-renders. The derived version computes inline — always correct, zero overhead. + +### Anti-Pattern Reference + +When naming exposes a problem, fix the structure — not the name: + +| Difficult Name | Signal | Fix | +|----------------|--------|-----| +| `syncDerivedValue` | Derived state stored in state | Compute during render | +| `resetFormOnSubmit` | Interaction logic in effect | Move to event handler | +| `notifyParentOfStateChange` | Upward data flow via effect | Lift state or callback to source | +| `updateStateBasedOnOtherState` | State-to-state sync | Derive or consolidate state | +| `doMultipleThingsOnMount` | Multiple concerns | Split into separate effects | + +These map directly to patterns in React's [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect). + +### The Naming Test + +When writing or reviewing an effect, apply this sequence: + +1. **Can you name it in 2-4 words?** → Good effect, name it +2. **Does the name require "and"?** → Split into separate effects +3. **Does the name describe state-to-state sync?** → Derive during render or move to event handler +4. **Can't think of a name at all?** → The effect is doing too much or shouldn't be an effect + +Effects that survive this test tend to synchronize with external systems: `connectToWebSocket`, `initializeMapSDK`, `subscribeToGeolocation`. The verbs are concrete and the targets are external. + +--- + +## Naming Conventions + +### Verb Vocabulary + +Use verb phrases that describe what the effect synchronizes with: + +| Verb | Signals | Example | +|------|---------|---------| +| `connect` / `subscribe` / `listen` | Event-based external sync | `connectToWebSocket` | +| `fetch` / `load` | Data retrieval | `fetchInitialStock` | +| `initialize` / `setup` | One-time setup | `initializeMapSDK` | +| `synchronize` / `apply` | External system sync | `synchronizeMapViewport` | +| `notify` / `report` | Outbound communication | `notifyAnalyticsOfPageView` | +| `reset` / `clear` | State cleanup on change | `resetStockOnLocationChange` | +| `track` / `observe` | Monitoring | `trackWindowWidth` | + +Effects with clear, concrete names tend to be legitimate synchronization points. Names that sound like internal state management (`updateStateBasedOnProps`, `syncDerivedValue`) usually signal code that belongs elsewhere. + +### Naming Cleanup Functions + +When teardown does non-trivial work, name it for symmetry: + +```tsx +useEffect(function pollServerForUpdates() { + const intervalId = setInterval(() => { + fetch(`/api/status/${serverId}`) + .then((res) => res.json()) + .then(setServerStatus); + }, 5000); + + return function stopPollingServer() { + clearInterval(intervalId); + }; +}, [serverId]); +``` + +The symmetry between `pollServerForUpdates` and `stopPollingServer` makes both halves immediately clear. For simple cleanup (clearing a single listener or timeout), naming the return function is optional. + +### Naming Other Hooks + +The same pattern improves readability in `useMemo` and `useCallback`: + +```tsx +const sortedItems = useMemo(function sortItemsByPrice() { + return [...items].sort((a, b) => a.price - b.price); +}, [items]); + +const stableOnChange = useCallback(function handleInputChange(e: ChangeEvent) { + validate(e.target.value); + onChange(e.target.value); +}, [validate, onChange]); +``` + +`useEffect` benefits most because effects run at non-obvious times, have hidden cleanup semantics, and require reconstructing trigger conditions from dependency arrays. But any hook taking a callback gains readability from a name. + +### Named Effects Inside Custom Hooks + +Custom hooks and named effects solve different problems — use both: + +- **Name inline** when the effect is single-use with no associated state +- **Extract to custom hook** when the effect manages its own state and may be reused +- **Always name the function inside the custom hook too** — custom hooks can have multiple effects + +```tsx +function useWindowWidth() { + const [width, setWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 0 + ); + + useEffect(function trackWindowWidth() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return width; +} +``` + +The custom hook name (`useWindowWidth`) describes what the consumer gets. The effect name (`trackWindowWidth`) describes what the implementation does. Both names serve different audiences: the hook name serves component authors, the effect name serves debuggers. + +--- + +## Tooling + +### ESLint Enforcement + +Two community plugins can enforce named effect callbacks: + +**eslint-plugin-goodeffects** — rule `enforce-named-effect-callbacks` fails on anonymous arrows in `useEffect`: + +```json +{ + "plugins": ["goodeffects"], + "rules": { + "goodeffects/enforce-named-effect-callbacks": "warn" + } +} +``` + +**eslint-plugin-use-encapsulation** — rule `prefer-custom-hooks` requires all hook calls to live inside custom hooks (stricter; enforces extraction, not just naming). Based on Kyle Shevlin's [useEncapsulation](https://kyleshevlin.com/use-encapsulation/) pattern. + +### Technical Notes + +**Named callbacks appear in JavaScript engine stack traces.** Chrome, Firefox, Node.js, and error monitoring tools (Sentry, Datadog, Replay.io) all display the function name instead of `(anonymous)`. This is the primary debugging benefit. + +**Named callbacks do not change React DevTools display.** DevTools extracts hook names via source-map AST parsing from variable assignments (e.g., `const [count, setCount] = useState(0)` shows "count"). Since `useEffect` returns nothing and has no variable assignment, DevTools shows "Effect" regardless of whether the callback is named. This is a common misconception. + +**No performance difference.** Named and anonymous functions are equivalent at runtime. Both create a new function reference on each render. Use `useCallback` for referential stability — naming is purely for readability and debugging. + +**React docs use anonymous arrows.** React's official documentation uses anonymous arrow functions in all `useEffect` examples for brevity and pedagogical clarity. This skill recommends named expressions for production code, where the readability and debugging benefits justify the slightly longer syntax. + +--- + +## References + +- [Neciu Dan — Start Naming Your useEffect Functions](https://neciudan.dev/name-your-effects) +- [React — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +- [React — Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) +- [Kyle Shevlin — useEncapsulation](https://kyleshevlin.com/use-encapsulation/) +- [Sergio Xalambrí — Pro Tip: Name Your useEffect Functions](https://sergiodxa.com/articles/pro-tip-name-your-useeffect-functions) diff --git a/skills/react-named-effects/README.md b/skills/react-named-effects/README.md new file mode 100644 index 00000000..fcaad285 --- /dev/null +++ b/skills/react-named-effects/README.md @@ -0,0 +1,33 @@ +# react-named-effects + +A skill for AI coding agents that teaches naming `useEffect`, `useCallback`, and `useMemo` callbacks with function expressions instead of anonymous arrows. + +## What It Covers + +- **Core pattern**: Replace `useEffect(() => { ... })` with `useEffect(function descriptiveName() { ... })` +- **Diagnostics**: How naming reveals effects that should be split, eliminated, or extracted +- **Conventions**: Naming vocabulary, cleanup functions, other hooks, custom hook integration +- **Tooling**: ESLint enforcement, stack trace behavior, React DevTools facts + +## Structure + +- `SKILL.md` — Entry point with core pattern and quick reference +- `AGENTS.md` — Full compiled guide with all references expanded +- `references/diagnostics.md` — Anti-pattern detection framework +- `references/conventions.md` — Naming vocabulary and tooling + +## Installation + +**Claude Code:** +```bash +cp -r skills/react-named-effects ~/.claude/skills/ +``` + +**claude.ai:** +Add the skill to project knowledge or paste SKILL.md contents. + +## Based On + +- [Start naming your useEffect functions](https://neciudan.dev/name-your-effects) by Neciu Dan +- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) — React docs +- [useEncapsulation](https://kyleshevlin.com/use-encapsulation/) by Kyle Shevlin diff --git a/skills/react-named-effects/SKILL.md b/skills/react-named-effects/SKILL.md new file mode 100644 index 00000000..5c05c022 --- /dev/null +++ b/skills/react-named-effects/SKILL.md @@ -0,0 +1,79 @@ +--- +name: react-named-effects +description: Name useEffect, useCallback, and useMemo callbacks with function expressions instead of anonymous arrows. Use when writing, reviewing, or refactoring React components with multiple effects, debugging unclear stack traces, or improving hook readability. Triggers on effect-heavy components, hook refactoring, code review, and stack trace debugging. +license: MIT +metadata: + author: community + version: "1.0.0" +--- + +# Named Effects + +Replace anonymous arrows in `useEffect` with named function expressions. The name documents intent, improves stack traces, and acts as a diagnostic — effects that resist clear naming have structural problems. + +> **Note:** React's official documentation uses anonymous arrows in all `useEffect` examples for brevity. This skill recommends named expressions for production code where readability and debugging outweigh conciseness — the same reason we name variables instead of using single letters. + +```tsx +// Anonymous — tells you nothing +useEffect(() => { + const ws = new WebSocket(`wss://api/${id}`); + ws.onmessage = (e) => setData(JSON.parse(e.data)); + return () => ws.close(); +}, [id]); + +// Named — intent is immediate +useEffect(function connectToWebSocket() { + const ws = new WebSocket(`wss://api/${id}`); + ws.onmessage = (e) => setData(JSON.parse(e.data)); + return () => ws.close(); +}, [id]); +``` + +## Why This Matters + +**Stack traces improve.** Anonymous effects show `at (anonymous) @ InventorySync.tsx:14`. Named effects show `at connectToWebSocket @ InventorySync.tsx:14`. This works in Chrome, Firefox, Node.js, Sentry, and Datadog — the name comes from the JavaScript engine, not React. + +**Component data flow becomes scannable.** Four named effects tell you everything without reading a single line of implementation: + +```tsx +useEffect(function connectToInventoryWebSocket() { /* ... */ }, [warehouseId]); +useEffect(function fetchInitialStock() { /* ... */ }, [warehouseId, locationId, connected]); +useEffect(function resetStockOnLocationChange() { /* ... */ }, [locationId]); +useEffect(function synchronizeDocumentTitle() { /* ... */ }, [stock]); +``` + +**Naming reveals structural problems.** If a name requires "and," the effect should be split. If the name sounds like state shuffling (`syncDerivedValue`, `updateStateFromProps`), the effect probably shouldn't exist. See `references/diagnostics.md` for the full diagnostic framework. + +## When to Apply + +Name every effect by default. It costs nothing and the benefits compound. Especially valuable when: +- A component has 2 or more effects +- An effect body exceeds a few lines +- Reviewing or debugging unfamiliar code +- Triaging errors in monitoring tools (Sentry, Datadog) + +## Quick Reference + +| Verb | Signals | Example | +|------|---------|---------| +| `connect` / `subscribe` | Event-based external sync | `connectToWebSocket` | +| `fetch` / `load` | Data retrieval | `fetchInitialStock` | +| `initialize` / `setup` | One-time setup | `initializeMapSDK` | +| `synchronize` / `apply` | External system sync | `synchronizeMapViewport` | +| `reset` / `clear` | State cleanup on change | `resetStockOnLocationChange` | +| `track` / `observe` | Monitoring | `trackWindowWidth` | + +For cleanup function naming, custom hook patterns, ESLint enforcement, and technical notes, see `references/conventions.md`. + +## Deeper Guidance + +- **Refactoring or reviewing effects?** → `references/diagnostics.md` — anti-pattern detection, split signals, effects that shouldn't exist +- **Need naming patterns or tooling?** → `references/conventions.md` — verb vocabulary, cleanup naming, useCallback/useMemo, custom hooks, ESLint plugins, technical notes + +## References + +- [Neciu Dan — Start Naming Your useEffect Functions](https://neciudan.dev/name-your-effects) +- [React — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +- [React — Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) +- [Kyle Shevlin — useEncapsulation](https://kyleshevlin.com/use-encapsulation/) +- [Sergio Xalambrí — Pro Tip: Name Your useEffect Functions](https://sergiodxa.com/articles/pro-tip-name-your-useeffect-functions) diff --git a/skills/react-named-effects/metadata.json b/skills/react-named-effects/metadata.json new file mode 100644 index 00000000..fe650ebb --- /dev/null +++ b/skills/react-named-effects/metadata.json @@ -0,0 +1,13 @@ +{ + "version": "1.0.0", + "organization": "Community", + "date": "April 2026", + "abstract": "Guide for naming useEffect, useCallback, and useMemo callbacks with function expressions instead of anonymous arrows. Improves stack traces, makes component data flow scannable, and acts as a diagnostic surface for identifying effects that should be split, eliminated, or extracted into custom hooks.", + "references": [ + "https://neciudan.dev/name-your-effects", + "https://react.dev/learn/you-might-not-need-an-effect", + "https://react.dev/learn/reusing-logic-with-custom-hooks", + "https://kyleshevlin.com/use-encapsulation/", + "https://sergiodxa.com/articles/pro-tip-name-your-useeffect-functions" + ] +} diff --git a/skills/react-named-effects/references/conventions.md b/skills/react-named-effects/references/conventions.md new file mode 100644 index 00000000..99b3107c --- /dev/null +++ b/skills/react-named-effects/references/conventions.md @@ -0,0 +1,107 @@ +# Naming Conventions and Tooling + +## Verb Vocabulary + +Use verb phrases that describe what the effect synchronizes with: + +| Verb | Signals | Example | +|------|---------|---------| +| `connect` / `subscribe` / `listen` | Event-based external sync | `connectToWebSocket` | +| `fetch` / `load` | Data retrieval | `fetchInitialStock` | +| `initialize` / `setup` | One-time setup | `initializeMapSDK` | +| `synchronize` / `apply` | External system sync | `synchronizeMapViewport` | +| `notify` / `report` | Outbound communication | `notifyAnalyticsOfPageView` | +| `reset` / `clear` | State cleanup on change | `resetStockOnLocationChange` | +| `track` / `observe` | Monitoring | `trackWindowWidth` | + +Effects with clear, concrete names tend to be legitimate synchronization points. Names that sound like internal state management (`updateStateBasedOnProps`, `syncDerivedValue`) usually signal code that belongs elsewhere — see `diagnostics.md`. + +## Naming Cleanup Functions + +When teardown does non-trivial work, name it for symmetry: + +```tsx +useEffect(function pollServerForUpdates() { + const intervalId = setInterval(() => { + fetch(`/api/status/${serverId}`) + .then((res) => res.json()) + .then(setServerStatus); + }, 5000); + + return function stopPollingServer() { + clearInterval(intervalId); + }; +}, [serverId]); +``` + +The symmetry between `pollServerForUpdates` and `stopPollingServer` makes both halves immediately clear. For simple cleanup (clearing a single listener or timeout), naming the return function is optional. + +## Naming Other Hooks + +The same pattern improves readability in `useMemo` and `useCallback`: + +```tsx +const sortedItems = useMemo(function sortItemsByPrice() { + return [...items].sort((a, b) => a.price - b.price); +}, [items]); + +const stableOnChange = useCallback(function handleInputChange(e: ChangeEvent) { + validate(e.target.value); + onChange(e.target.value); +}, [validate, onChange]); +``` + +`useEffect` benefits most because effects run at non-obvious times, have hidden cleanup semantics, and require reconstructing trigger conditions from dependency arrays. But any hook taking a callback gains readability from a name. + +## Named Effects Inside Custom Hooks + +Custom hooks and named effects solve different problems — use both: + +- **Name inline** when the effect is single-use with no associated state +- **Extract to custom hook** when the effect manages its own state and may be reused +- **Always name the function inside the custom hook too** — custom hooks can have multiple effects + +```tsx +function useWindowWidth() { + const [width, setWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 0 + ); + + useEffect(function trackWindowWidth() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return width; +} +``` + +The custom hook name (`useWindowWidth`) describes what the consumer gets. The effect name (`trackWindowWidth`) describes what the implementation does. Both names serve different audiences: the hook name serves component authors, the effect name serves debuggers. + +## ESLint Enforcement + +Two community plugins can enforce named effect callbacks: + +**eslint-plugin-goodeffects** — rule `enforce-named-effect-callbacks` fails on anonymous arrows in `useEffect`: + +```json +{ + "plugins": ["goodeffects"], + "rules": { + "goodeffects/enforce-named-effect-callbacks": "warn" + } +} +``` + +**eslint-plugin-use-encapsulation** — rule `prefer-custom-hooks` requires all hook calls to live inside custom hooks (stricter; enforces extraction, not just naming). Based on Kyle Shevlin's [useEncapsulation](https://kyleshevlin.com/use-encapsulation/) pattern. + +## Technical Notes + +**Named callbacks appear in JavaScript engine stack traces.** Chrome, Firefox, Node.js, and error monitoring tools (Sentry, Datadog, Replay.io) all display the function name instead of `(anonymous)`. This is the primary debugging benefit. + +**Named callbacks do not change React DevTools display.** DevTools extracts hook names via source-map AST parsing from variable assignments (e.g., `const [count, setCount] = useState(0)` shows "count"). Since `useEffect` returns nothing and has no variable assignment, DevTools shows "Effect" regardless of whether the callback is named. This is a common misconception. + +**No performance difference.** Named and anonymous functions are equivalent at runtime. Both create a new function reference on each render. Use `useCallback` for referential stability — naming is purely for readability and debugging. + +**React docs use anonymous arrows.** React's official documentation uses anonymous arrow functions in all `useEffect` examples for brevity and pedagogical clarity. This skill recommends named expressions for production code, where the readability and debugging benefits justify the slightly longer syntax. diff --git a/skills/react-named-effects/references/diagnostics.md b/skills/react-named-effects/references/diagnostics.md new file mode 100644 index 00000000..b1678bba --- /dev/null +++ b/skills/react-named-effects/references/diagnostics.md @@ -0,0 +1,77 @@ +# Diagnostics: What Naming Reveals + +Naming effects isn't just style — it's a diagnostic tool. When an effect resists a clean name, the name is telling you the effect has a structural problem. + +## Split Signal: "And" in the Name + +If the best name contains "and," the effect handles two concerns: + +```tsx +// Name requires "and" — two unrelated concerns +useEffect(function syncWidthAndApplyTheme() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + if (user?.preferences?.theme) { + document.body.className = user.preferences.theme; + } + return () => window.removeEventListener('resize', handleResize); +}, [user?.preferences?.theme]); +``` + +```tsx +// Two focused effects +useEffect(function trackWindowWidth() { + const handleResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); +}, []); + +useEffect(function applyUserTheme() { + if (user?.preferences?.theme) { + document.body.className = user.preferences.theme; + } +}, [user?.preferences?.theme]); +``` + +After splitting, each effect has a single clear name and its own dependency array. Dependencies that seemed unrelated in the combined effect now make sense in isolation. + +## Elimination Signal: State Shuffling Names + +If the best name sounds like internal state management, the effect likely shouldn't exist: + +```tsx +// Vague name — signals derived state stored in state +useEffect(function syncFullName() { + setFullName(`${firstName} ${lastName}`); +}, [firstName, lastName]); + +// Derive during render instead — no effect, no extra render cycle +const fullName = `${firstName} ${lastName}`; +``` + +The effect version triggers an extra render: React renders, runs the effect, calls `setFullName`, re-renders. The derived version computes inline — always correct, zero overhead. + +## Anti-Pattern Reference + +When naming exposes a problem, fix the structure — not the name: + +| Difficult Name | Signal | Fix | +|----------------|--------|-----| +| `syncDerivedValue` | Derived state stored in state | Compute during render | +| `resetFormOnSubmit` | Interaction logic in effect | Move to event handler | +| `notifyParentOfStateChange` | Upward data flow via effect | Lift state or callback to source | +| `updateStateBasedOnOtherState` | State-to-state sync | Derive or consolidate state | +| `doMultipleThingsOnMount` | Multiple concerns | Split into separate effects | + +These map directly to patterns in React's [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect). + +## The Naming Test + +When writing or reviewing an effect, apply this sequence: + +1. **Can you name it in 2-4 words?** → Good effect, name it +2. **Does the name require "and"?** → Split into separate effects +3. **Does the name describe state-to-state sync?** → Derive during render or move to event handler +4. **Can't think of a name at all?** → The effect is doing too much or shouldn't be an effect + +Effects that survive this test tend to synchronize with external systems: `connectToWebSocket`, `initializeMapSDK`, `subscribeToGeolocation`. The verbs are concrete and the targets are external.