.config/opencode/skills/react-useeffect-avoid/SKILL.md
Guides when NOT to use useEffect and suggests better alternatives. Use when reviewing React code, troubleshooting performance, or considering useEffect for derived state or form resets.
npx skillsauth add klen/dotfiles react-useeffect-avoidInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
useEffect is an escape hatch for synchronizing with external systems, not a general-purpose tool for state management or event handling.
Modern React patterns prioritize one-way data flow and event-driven updates over effect-based synchronization to avoid performance penalties and complex synchronization bugs.
Problem: Computing one state value from another using useEffect causes unnecessary
double-renders and complexity.
// ❌ BAD: Double render cycle
function FilteredList({ items }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(item => item.name.includes(query)));
}, [items, query]); // Renders twice on every input
return <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
✅ Solution: Calculate derived state directly during render.
// ✅ GOOD: Single render, no effect needed
function FilteredList({ items }) {
const [query, setQuery] = useState('');
// Calculated every render - no state, no effect
const filtered = items.filter(item => item.name.includes(query));
return <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
Why it's better:
Problem: Watching a prop change and manually resetting state causes stale data display and extra renders.
// ❌ BAD: Shows old state briefly, then new
function UserForm({ userId }) {
const [userName, setUserName] = useState('');
const [email, setEmail] = useState('');
useEffect(() => {
setUserName(''); // First render shows old userName
setEmail(''); // Then this effect runs to reset
}, [userId]); // Double render every userId change
return (
<form>
<input value={userName} onChange={e => setUserName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}
✅ Solution A: Key Prop (Preferred)
// ✅ GOOD: React tears down and rebuilds component
function App() {
const [userId, setUserId] = useState(1);
return (
<div>
<button onClick={() => setUserId(prev => prev + 1)}>Next User</button>
{/* Different key = different component instance */}
<UserForm key={userId} userId={userId} />
</div>
);
}
✅ Solution B: Derived State (When you only need partial reset)
// ✅ GOOD: Only reset specific values, keep others
function UserForm({ userId }) {
const [userName, setUserName] = useSyncExternalStore(
() => ({ onSet: setUserName }), // Reset when userId changes
{ value: '' }
);
const [email, setEmail] = useState('');
// Email persists, userName resets
return <form>...</form>;
}
⚠️ Caveat: The key prop approach remounts the entire component.
Use when you need a complete state reset.
For partial resets, consider derived state patterns.
Problem: Multiple effects triggering each other create cascading renders that are hard to debug.
// ❌ BAD: Effects trigger effects
function OrderForm() {
const [formData, setFormData] = useState({});
const [validated, setValidated] = useState(false);
const [error, setError] = useState(null);
// Effect 1: Validate when formData changes
useEffect(() => {
setValidated(validateData(formData));
}, [formData]);
// Effect 2: Set error when validation changes
useEffect(() => {
setError(validated ? null : 'Invalid');
}, [validated]);
// Effect 3: Submit when no error
useEffect(() => {
if (!error && validated) submitOrder(formData);
}, [error, validated, formData]);
}
✅ Solution: Handle all logic in event handler.
// ✅ GOOD: One handler, one render, clear flow
function OrderForm() {
const [formData, setFormData] = useState({});
const handleSubmit = () => {
// All logic happens atomically
if (validateData(formData)) {
submitOrder(formData); // No intermediate states
} else {
setError('Invalid form'); // Set directly, no cascade
}
};
return <button onClick={handleSubmit}>Submit</button>;
}
Problem: Using a flag to trigger effects loses user intent and creates fragile code.
// ❌ BAD: Intent is lost, hard to follow
function LoginForm() {
const [username, setUsername] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = () => {
setSubmitted(true); // Just a flag, no actual logic
};
useEffect(() => {
if (submitted) {
login(username); // What triggered this? Which submit was it?
}
}, [submitted, username]);
}
✅ Solution: Perform action directly in handler.
// ✅ GOOD: Clear intent, direct action
function LoginForm() {
const [username, setUsername] = useState('');
const handleSubmit = async (e) => {
e.preventDefault(); // User action context preserved
// Actual logic here, not delayed
try {
await login(username);
} catch (error) {
console.error('Login failed:', error);
}
};
return <form onSubmit={handleSubmit}>...</form>;
}
Problem: Creating filtered lists via effects is a common mistake that causes extra renders.
// ❌ BAD: Unnecessary effect
function ProductList({ products }) {
const [visibleProducts, setVisibleProducts] = useState([]);
useEffect(() => {
setVisibleProducts(products.filter(p => p.inStock));
}, [products]); // Runs twice whenever products change
}
✅ Solution: Filter directly during render.
// ✅ GOOD: Immediate, single render
function ProductList({ products }) {
const visibleProducts = products.filter(p => p.inStock);
}
Problem: Using useState + useEffect
for external subscriptions causes "tearing" in concurrent rendering.
// ❌ BAD: UI can show inconsistent state during concurrent updates
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
}
✅ Solution: Use useSyncExternalStore for external state.
// ✅ GOOD: Prevents tearing, single source of truth
function WindowSize() {
const width = useSyncExternalStore(
// Subscribe - return cleanup function
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
// Get snapshot
() => window.innerWidth,
// Server fallback
() => 1200
);
}
Why it's better:
Problem: Empty dependency array with state inside effect causes stale bugs.
// ❌ BAD: Id never updates, always fetches user 1
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/user/1`).then(res => res.json()).then(setUser);
}, [ ]); // Missing userId dependency!
}
✅ Solution: Include all dependencies or use proper patterns.
// ✅ GOOD: Correct dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/user/${userId}`).then(res => res.json()).then(setUser);
}, [userId]); // Properly tracks changes
}
Better: Move to event-driven fetching or data libraries.
// ✅ EVEN BETTER: No effect, just loader
function UserProfile({ userId }) {
const user = use(userId); // React 19's use API
// Or use React Query, SWR, etc.
}
Need to sync with external system?
├─ Yes (browser APIs, websockets, timers)
│ └─ Use useEffect
│
└─ No (pure React application logic)
├─ Derived state calculation?
│ ├─ Yes → Calculate during render
│ └─ No → Continue...
│
├─ User action triggered?
│ ├─ Yes → Use event handler
│ └─ No → Continue...
│
├─ State reset needed?
│ ├─ Yes → Use key prop
│ └─ No → Continue...
│
└─ Really need effect after re-think?
└─ Yes → Use useState/useReducer/setState pattern
// BAD
function Modal() {
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);
}
✅ Better: Use refs or layout effects when needed.
// BAD
function App({ data }) {
useEffect(() => {
console.log('Data changed:', data);
}, [data]);
}
✅ Better: Log in event handler or use React DevTools profiling.
// BAD
function Widget() {
const [initialized, setInitialized] = useState(false);
useEffect(() => {
someLibrary.init();
setInitialized(true);
}, []);
}
✅ Better: Initialize outside component effect:
// GOOD
let initialized = false;
function Widget() {
if (!initialized) {
someLibrary.init();
initialized = true;
}
}
Or use library-specific initialization patterns.
| Scenario | Problem | Alternative |
|----------|---------|-------------|
| Derived state | Double render | Calculate during render |
| State resets | Stale data | Use key prop |
| User actions | Lost intent | Event handlers |
| List filtering | Extra renders | Filter in render |
| Browser APIs | Tearing bugs (concurrent) | useSyncExternalStore |
| Form submission | Fragile flag pattern | Direct async handler |
| Data fetching | Manual cache management | React Query, SWR, Suspense |
React 19 introduces the use API for reading resources in render:
// React 19+ - Direct resource reading
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Reads promise directly
return <div>{user.name}</div>;
}
This eliminates many data-fetching useEffect patterns entirely.
tools
Anti-patterns and mistakes to avoid as a product manager. Use when evaluating leadership behaviors, improving team dynamics, reflecting on management practices, or onboarding new product managers.
development
Guides proper usage of TypeScript's satisfies operator vs type annotations. Use this skill when deciding between type annotations (colon) and satisfies, validating object shapes while preserving literal types, or troubleshooting type inference issues.
development
Guides when to use interface vs type in TypeScript. Use this skill when defining object types, extending types, or choosing between interface and type aliases.
development
Guides TypeScript best practices for type safety, code organization, and maintainability. Use this skill when configuring TypeScript projects, deciding on typing strategies, writing async code, or reviewing TypeScript code quality.