chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# React useEffect Best Practices
|
||||
|
||||
A comprehensive guide teaching when to use `useEffect` in React, and more importantly, when NOT to use it. This skill is based on official React documentation and provides practical alternatives to common useEffect anti-patterns.
|
||||
|
||||
## Purpose
|
||||
|
||||
Effects are an **escape hatch** from React's reactive paradigm. They let you synchronize with external systems like browser APIs, third-party widgets, or network requests. However, many developers overuse Effects for tasks that React handles better through other means.
|
||||
|
||||
This skill helps you:
|
||||
- Identify when you truly need an Effect vs. when you don't
|
||||
- Recognize common anti-patterns and their fixes
|
||||
- Apply better alternatives like `useMemo`, `key` prop, and event handlers
|
||||
- Write Effects that are clean, maintainable, and free from race conditions
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you're:
|
||||
- Writing or reviewing `useEffect` code
|
||||
- Using `useState` to store derived values
|
||||
- Implementing data fetching or subscriptions
|
||||
- Synchronizing state between components
|
||||
- Facing bugs with stale data or race conditions
|
||||
- Wondering if your Effect is necessary
|
||||
|
||||
**Trigger phrases:**
|
||||
- "Should I use useEffect for this?"
|
||||
- "How do I fix this useEffect?"
|
||||
- "My Effect is causing too many re-renders"
|
||||
- "Data fetching with useEffect"
|
||||
- "Reset state when props change"
|
||||
- "Derived state from props"
|
||||
|
||||
## How It Works
|
||||
|
||||
This skill provides guidance through three key resources:
|
||||
|
||||
1. **Quick Reference Table** - Fast lookup for common scenarios with DO/DON'T patterns
|
||||
2. **Decision Tree** - Visual flowchart to determine the right approach
|
||||
3. **Detailed Anti-Patterns** - 9 common mistakes with explanations and fixes
|
||||
4. **Better Alternatives** - 8 proven patterns to replace unnecessary Effects
|
||||
|
||||
The skill teaches you to ask the right questions:
|
||||
- Is there an external system involved?
|
||||
- Am I responding to a user event or component appearance?
|
||||
- Can this value be calculated during render?
|
||||
- Do I need to reset state when a prop changes?
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Quick Reference Guide
|
||||
|
||||
Visual table showing the DO/DON'T for common scenarios:
|
||||
- Derived state from props/state
|
||||
- Expensive calculations
|
||||
- Resetting state on prop change
|
||||
- User event responses
|
||||
- Notifying parent components
|
||||
- Data fetching
|
||||
|
||||
### 2. Decision Tree
|
||||
|
||||
Clear flowchart that guides you from "Need to respond to something?" to the correct solution:
|
||||
- User interaction → Event handler
|
||||
- Component appeared → Effect (for external sync/analytics)
|
||||
- Derived value needed → Calculate during render (+ useMemo if expensive)
|
||||
- Reset state on prop change → Key prop
|
||||
|
||||
### 3. Anti-Pattern Recognition
|
||||
|
||||
Detailed examples of 9 common mistakes:
|
||||
1. Redundant state for derived values
|
||||
2. Filtering/transforming data in Effect
|
||||
3. Resetting state on prop change
|
||||
4. Event-specific logic in Effect
|
||||
5. Chains of Effects
|
||||
6. Notifying parent via Effect
|
||||
7. Passing data up to parent
|
||||
8. Fetching without cleanup (race conditions)
|
||||
9. App initialization in Effect
|
||||
|
||||
Each anti-pattern includes:
|
||||
- Bad example with explanation
|
||||
- Good example with fix
|
||||
- Why the anti-pattern is problematic
|
||||
|
||||
### 4. Better Alternatives
|
||||
|
||||
8 proven patterns to replace unnecessary Effects:
|
||||
1. Calculate during render for derived state
|
||||
2. `useMemo` for expensive calculations
|
||||
3. `key` prop to reset state
|
||||
4. Store ID instead of object for stable references
|
||||
5. Event handlers for user actions
|
||||
6. `useSyncExternalStore` for external stores
|
||||
7. Lifting state up for shared state
|
||||
8. Custom hooks for data fetching with cleanup
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Derived State
|
||||
|
||||
**Bad - Unnecessary Effect:**
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
}
|
||||
```
|
||||
|
||||
**Good - Calculate during render:**
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const fullName = firstName + ' ' + lastName; // Just compute it
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Resetting State
|
||||
|
||||
**Bad - Effect to reset:**
|
||||
```tsx
|
||||
function ProfilePage({ userId }) {
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setComment('');
|
||||
}, [userId]);
|
||||
}
|
||||
```
|
||||
|
||||
**Good - Key prop:**
|
||||
```tsx
|
||||
function ProfilePage({ userId }) {
|
||||
return <Profile userId={userId} key={userId} />;
|
||||
}
|
||||
|
||||
function Profile({ userId }) {
|
||||
const [comment, setComment] = useState(''); // Resets automatically
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Data Fetching with Cleanup
|
||||
|
||||
**Bad - Race condition:**
|
||||
```tsx
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchResults(query).then(json => {
|
||||
setResults(json); // "hello" response may arrive after "hell"
|
||||
});
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
**Good - Cleanup flag:**
|
||||
```tsx
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
fetchResults(query).then(json => {
|
||||
if (!ignore) setResults(json);
|
||||
});
|
||||
|
||||
return () => { ignore = true; };
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Event Handler Instead of Effect
|
||||
|
||||
**Bad - Effect watching state:**
|
||||
```tsx
|
||||
function ProductPage({ product, addToCart }) {
|
||||
useEffect(() => {
|
||||
if (product.isInCart) {
|
||||
showNotification(`Added ${product.name}!`);
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
function handleBuyClick() {
|
||||
addToCart(product);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Good - Handle in event:**
|
||||
```tsx
|
||||
function ProductPage({ product, addToCart }) {
|
||||
function handleBuyClick() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name}!`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## When You DO Need Effects
|
||||
|
||||
Effects are appropriate for:
|
||||
|
||||
- **Synchronizing with external systems** - Browser APIs, third-party widgets, non-React code
|
||||
- **Subscriptions** - WebSocket connections, global event listeners (prefer `useSyncExternalStore`)
|
||||
- **Analytics/logging** - Code that needs to run because the component displayed
|
||||
- **Data fetching** - With proper cleanup (or use your framework's built-in mechanism)
|
||||
|
||||
## When You DON'T Need Effects
|
||||
|
||||
Avoid Effects for:
|
||||
|
||||
1. **Transforming data for rendering** - Calculate at the top level instead
|
||||
2. **Handling user events** - Use event handlers where you know exactly what happened
|
||||
3. **Deriving state** - Just compute it: `const fullName = firstName + ' ' + lastName`
|
||||
4. **Chaining state updates** - Calculate all next state in the event handler
|
||||
5. **Notifying parent components** - Call the callback in the same event handler
|
||||
6. **Resetting state** - Use the `key` prop to create a fresh component instance
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Start Without an Effect
|
||||
|
||||
Before adding an Effect, ask: "Is there an external system involved?" If no, you probably don't need an Effect.
|
||||
|
||||
### 2. Prefer Derived State
|
||||
|
||||
If you can calculate a value from props or state, don't store it in state with an Effect updating it.
|
||||
|
||||
### 3. Use the Right Tool
|
||||
|
||||
- Expensive calculation → `useMemo`
|
||||
- User interaction → Event handler
|
||||
- Reset on prop change → `key` prop
|
||||
- External subscription → `useSyncExternalStore`
|
||||
- Shared state → Lift state up
|
||||
|
||||
### 4. Always Clean Up
|
||||
|
||||
If your Effect subscribes, fetches, or sets timers, return a cleanup function to prevent memory leaks and race conditions.
|
||||
|
||||
### 5. Avoid Effect Chains
|
||||
|
||||
Multiple Effects triggering each other causes unnecessary re-renders and makes code hard to follow. Calculate everything in one place (usually an event handler).
|
||||
|
||||
### 6. Test in Strict Mode
|
||||
|
||||
React 18+ Strict Mode mounts components twice in development to expose missing cleanup. If your Effect breaks, you need cleanup.
|
||||
|
||||
### 7. Consider Framework Solutions
|
||||
|
||||
For data fetching, prefer your framework's built-in solution (Next.js, Remix) or libraries (React Query, SWR) over manual Effects.
|
||||
|
||||
## Reference Files
|
||||
|
||||
This skill includes three detailed reference documents:
|
||||
|
||||
1. **SKILL.md** - Quick reference table and decision tree
|
||||
2. **anti-patterns.md** - 9 common mistakes with detailed explanations
|
||||
3. **alternatives.md** - 8 better alternatives with code examples
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Multiple Re-renders
|
||||
|
||||
**Symptom:** Component re-renders many times in quick succession.
|
||||
|
||||
**Cause:** Effect that sets state based on state it depends on, creating a loop.
|
||||
|
||||
**Fix:** Calculate the final value in an event handler or during render.
|
||||
|
||||
### Stale Data
|
||||
|
||||
**Symptom:** UI shows outdated values briefly before updating.
|
||||
|
||||
**Cause:** Using Effect to update derived state causes an extra render pass.
|
||||
|
||||
**Fix:** Calculate derived values during render instead of in state.
|
||||
|
||||
### Race Conditions
|
||||
|
||||
**Symptom:** Fast typing shows results for old queries after new ones.
|
||||
|
||||
**Cause:** Missing cleanup in data fetching Effect.
|
||||
|
||||
**Fix:** Use cleanup flag (`ignore` variable) or AbortController.
|
||||
|
||||
### Runs Twice in Development
|
||||
|
||||
**Symptom:** Effect runs twice on component mount in development.
|
||||
|
||||
**Cause:** React 18 Strict Mode intentionally mounts components twice to expose bugs.
|
||||
|
||||
**Fix:** Add proper cleanup. If it's app initialization that shouldn't run twice, use a module-level guard.
|
||||
|
||||
## Resources
|
||||
|
||||
This skill is based on:
|
||||
- [React Official Docs: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
||||
- [React Official Docs: Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects)
|
||||
- [React Official Docs: Lifecycle of Reactive Effects](https://react.dev/learn/lifecycle-of-reactive-effects)
|
||||
|
||||
## Summary
|
||||
|
||||
The golden rule: **Effects are an escape hatch from React.** If you're not synchronizing with an external system, you probably don't need an Effect.
|
||||
|
||||
Before writing `useEffect`, ask yourself:
|
||||
1. Is this responding to a user interaction? → Use event handler
|
||||
2. Is this a value I can calculate from props/state? → Calculate during render
|
||||
3. Is this resetting state when a prop changes? → Use key prop
|
||||
4. Is this synchronizing with an external system? → Use Effect with cleanup
|
||||
|
||||
Follow these patterns, and your React code will be more maintainable, performant, and bug-free.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: react-useeffect
|
||||
description: React useEffect best practices from official docs. Use when writing/reviewing useEffect, useState for derived values, data fetching, or state synchronization. Teaches when NOT to use Effect and better alternatives.
|
||||
---
|
||||
|
||||
# You Might Not Need an Effect
|
||||
|
||||
Effects are an **escape hatch** from React. They let you synchronize with external systems. If there is no external system involved, you shouldn't need an Effect.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | DON'T | DO |
|
||||
|-----------|-------|-----|
|
||||
| Derived state from props/state | `useState` + `useEffect` | Calculate during render |
|
||||
| Expensive calculations | `useEffect` to cache | `useMemo` |
|
||||
| Reset state on prop change | `useEffect` with `setState` | `key` prop |
|
||||
| User event responses | `useEffect` watching state | Event handler directly |
|
||||
| Notify parent of changes | `useEffect` calling `onChange` | Call in event handler |
|
||||
| Fetch data | `useEffect` without cleanup | `useEffect` with cleanup OR framework |
|
||||
|
||||
## When You DO Need Effects
|
||||
|
||||
- Synchronizing with **external systems** (non-React widgets, browser APIs)
|
||||
- **Subscriptions** to external stores (use `useSyncExternalStore` when possible)
|
||||
- **Analytics/logging** that runs because component displayed
|
||||
- **Data fetching** with proper cleanup (or use framework's built-in mechanism)
|
||||
|
||||
## When You DON'T Need Effects
|
||||
|
||||
1. **Transforming data for rendering** - Calculate at top level, re-runs automatically
|
||||
2. **Handling user events** - Use event handlers, you know exactly what happened
|
||||
3. **Deriving state** - Just compute it: `const fullName = firstName + ' ' + lastName`
|
||||
4. **Chaining state updates** - Calculate all next state in the event handler
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Need to respond to something?
|
||||
├── User interaction (click, submit, drag)?
|
||||
│ └── Use EVENT HANDLER
|
||||
├── Component appeared on screen?
|
||||
│ └── Use EFFECT (external sync, analytics)
|
||||
├── Props/state changed and need derived value?
|
||||
│ └── CALCULATE DURING RENDER
|
||||
│ └── Expensive? Use useMemo
|
||||
└── Need to reset state when prop changes?
|
||||
└── Use KEY PROP on component
|
||||
```
|
||||
|
||||
## Detailed Guidance
|
||||
|
||||
- [Anti-Patterns](./anti-patterns.md) - Common mistakes with fixes
|
||||
- [Better Alternatives](./alternatives.md) - useMemo, key prop, lifting state, useSyncExternalStore
|
||||
@@ -0,0 +1,258 @@
|
||||
# Better Alternatives to useEffect
|
||||
|
||||
## 1. Calculate During Render (Derived State)
|
||||
|
||||
For values derived from props or state, just compute them:
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
|
||||
// Runs every render - that's fine and intentional
|
||||
const fullName = firstName + ' ' + lastName;
|
||||
const isValid = firstName.length > 0 && lastName.length > 0;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use**: The value can be computed from existing props/state.
|
||||
|
||||
---
|
||||
|
||||
## 2. useMemo for Expensive Calculations
|
||||
|
||||
When computation is expensive, memoize it:
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
|
||||
function TodoList({ todos, filter }) {
|
||||
const visibleTodos = useMemo(
|
||||
() => getFilteredTodos(todos, filter),
|
||||
[todos, filter]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**How to know if it's expensive**:
|
||||
```tsx
|
||||
console.time('filter');
|
||||
const visibleTodos = getFilteredTodos(todos, filter);
|
||||
console.timeEnd('filter');
|
||||
// If > 1ms, consider memoizing
|
||||
```
|
||||
|
||||
**Note**: React Compiler can auto-memoize, reducing manual useMemo needs.
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Prop to Reset State
|
||||
|
||||
To reset ALL state when a prop changes, use key:
|
||||
|
||||
```tsx
|
||||
// Parent passes userId as key
|
||||
function ProfilePage({ userId }) {
|
||||
return (
|
||||
<Profile
|
||||
userId={userId}
|
||||
key={userId} // Different userId = different component instance
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile({ userId }) {
|
||||
// All state here resets when userId changes
|
||||
const [comment, setComment] = useState('');
|
||||
const [likes, setLikes] = useState([]);
|
||||
}
|
||||
```
|
||||
|
||||
**When to use**: You want a "fresh start" when an identity prop changes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Store ID Instead of Object
|
||||
|
||||
To preserve selection when list changes:
|
||||
|
||||
```tsx
|
||||
// BAD: Storing object that needs Effect to "adjust"
|
||||
function List({ items }) {
|
||||
const [selection, setSelection] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelection(null); // Reset when items change
|
||||
}, [items]);
|
||||
}
|
||||
|
||||
// GOOD: Store ID, derive object
|
||||
function List({ items }) {
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
|
||||
// Derived - no Effect needed
|
||||
const selection = items.find(item => item.id === selectedId) ?? null;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefit**: If item with selectedId exists in new list, selection preserved.
|
||||
|
||||
---
|
||||
|
||||
## 5. Event Handlers for User Actions
|
||||
|
||||
User clicks/submits/drags should be handled in event handlers, not Effects:
|
||||
|
||||
```tsx
|
||||
// Event handler knows exactly what happened
|
||||
function ProductPage({ product, addToCart }) {
|
||||
function handleBuyClick() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name}!`);
|
||||
analytics.track('product_added', { id: product.id });
|
||||
}
|
||||
|
||||
function handleCheckoutClick() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name}!`);
|
||||
navigateTo('/checkout');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Shared logic**: Extract a function, call from both handlers:
|
||||
|
||||
```tsx
|
||||
function buyProduct() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name}!`);
|
||||
}
|
||||
|
||||
function handleBuyClick() { buyProduct(); }
|
||||
function handleCheckoutClick() { buyProduct(); navigateTo('/checkout'); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. useSyncExternalStore for External Stores
|
||||
|
||||
For subscribing to external data (browser APIs, third-party stores):
|
||||
|
||||
```tsx
|
||||
// Instead of manual Effect subscription
|
||||
function useOnlineStatus() {
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
function update() { setIsOnline(navigator.onLine); }
|
||||
window.addEventListener('online', update);
|
||||
window.addEventListener('offline', update);
|
||||
return () => {
|
||||
window.removeEventListener('online', update);
|
||||
window.removeEventListener('offline', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isOnline;
|
||||
}
|
||||
|
||||
// Use purpose-built hook
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
function subscribe(callback) {
|
||||
window.addEventListener('online', callback);
|
||||
window.addEventListener('offline', callback);
|
||||
return () => {
|
||||
window.removeEventListener('online', callback);
|
||||
window.removeEventListener('offline', callback);
|
||||
};
|
||||
}
|
||||
|
||||
function useOnlineStatus() {
|
||||
return useSyncExternalStore(
|
||||
subscribe,
|
||||
() => navigator.onLine, // Client value
|
||||
() => true // Server value (SSR)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Lifting State Up
|
||||
|
||||
When two components need synchronized state, lift it to common ancestor:
|
||||
|
||||
```tsx
|
||||
// Instead of syncing via Effects between siblings
|
||||
function Parent() {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input value={value} onChange={setValue} />
|
||||
<Preview value={value} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Custom Hooks for Data Fetching
|
||||
|
||||
Extract fetch logic with proper cleanup:
|
||||
|
||||
```tsx
|
||||
function useData(url) {
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
setLoading(true);
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
if (!ignore) {
|
||||
setData(json);
|
||||
setError(null);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (!ignore) setError(err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!ignore) setLoading(false);
|
||||
});
|
||||
|
||||
return () => { ignore = true; };
|
||||
}, [url]);
|
||||
|
||||
return { data, error, loading };
|
||||
}
|
||||
|
||||
// Usage
|
||||
function SearchResults({ query }) {
|
||||
const { data, error, loading } = useData(`/api/search?q=${query}`);
|
||||
}
|
||||
```
|
||||
|
||||
**Better**: Use framework's data fetching (React Query, SWR, Next.js, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Summary: When to Use What
|
||||
|
||||
| Need | Solution |
|
||||
|------|----------|
|
||||
| Value from props/state | Calculate during render |
|
||||
| Expensive calculation | `useMemo` |
|
||||
| Reset all state on prop change | `key` prop |
|
||||
| Respond to user action | Event handler |
|
||||
| Sync with external system | `useEffect` with cleanup |
|
||||
| Subscribe to external store | `useSyncExternalStore` |
|
||||
| Share state between components | Lift state up |
|
||||
| Fetch data | Custom hook with cleanup / framework |
|
||||
@@ -0,0 +1,290 @@
|
||||
# useEffect Anti-Patterns
|
||||
|
||||
## 1. Redundant State for Derived Values
|
||||
|
||||
```tsx
|
||||
// BAD: Extra state + Effect for derived value
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
}
|
||||
|
||||
// GOOD: Calculate during rendering
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const fullName = firstName + ' ' + lastName; // Just compute it
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's bad**: Causes extra render pass with stale value, then re-renders with updated value.
|
||||
|
||||
---
|
||||
|
||||
## 2. Filtering/Transforming Data in Effect
|
||||
|
||||
```tsx
|
||||
// BAD: Effect to filter list
|
||||
function TodoList({ todos, filter }) {
|
||||
const [visibleTodos, setVisibleTodos] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleTodos(getFilteredTodos(todos, filter));
|
||||
}, [todos, filter]);
|
||||
}
|
||||
|
||||
// GOOD: Filter during render (memoize if expensive)
|
||||
function TodoList({ todos, filter }) {
|
||||
const visibleTodos = useMemo(
|
||||
() => getFilteredTodos(todos, filter),
|
||||
[todos, filter]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Resetting State on Prop Change
|
||||
|
||||
```tsx
|
||||
// BAD: Effect to reset state
|
||||
function ProfilePage({ userId }) {
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setComment('');
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
// GOOD: Use key prop
|
||||
function ProfilePage({ userId }) {
|
||||
return <Profile userId={userId} key={userId} />;
|
||||
}
|
||||
|
||||
function Profile({ userId }) {
|
||||
const [comment, setComment] = useState(''); // Resets automatically
|
||||
}
|
||||
```
|
||||
|
||||
**Why key works**: React treats components with different keys as different components, recreating state.
|
||||
|
||||
---
|
||||
|
||||
## 4. Event-Specific Logic in Effect
|
||||
|
||||
```tsx
|
||||
// BAD: Effect for button click result
|
||||
function ProductPage({ product, addToCart }) {
|
||||
useEffect(() => {
|
||||
if (product.isInCart) {
|
||||
showNotification(`Added ${product.name}!`);
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
function handleBuyClick() {
|
||||
addToCart(product);
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Handle in event handler
|
||||
function ProductPage({ product, addToCart }) {
|
||||
function handleBuyClick() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name}!`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's bad**: Effect fires on page refresh (isInCart is true), showing notification unexpectedly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chains of Effects
|
||||
|
||||
```tsx
|
||||
// BAD: Effects triggering each other
|
||||
function Game() {
|
||||
const [card, setCard] = useState(null);
|
||||
const [goldCardCount, setGoldCardCount] = useState(0);
|
||||
const [round, setRound] = useState(1);
|
||||
const [isGameOver, setIsGameOver] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (card?.gold) setGoldCardCount(c => c + 1);
|
||||
}, [card]);
|
||||
|
||||
useEffect(() => {
|
||||
if (goldCardCount > 3) {
|
||||
setRound(r => r + 1);
|
||||
setGoldCardCount(0);
|
||||
}
|
||||
}, [goldCardCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (round > 5) setIsGameOver(true);
|
||||
}, [round]);
|
||||
}
|
||||
|
||||
// GOOD: Calculate in event handler
|
||||
function Game() {
|
||||
const [card, setCard] = useState(null);
|
||||
const [goldCardCount, setGoldCardCount] = useState(0);
|
||||
const [round, setRound] = useState(1);
|
||||
const isGameOver = round > 5; // Derived!
|
||||
|
||||
function handlePlaceCard(nextCard) {
|
||||
if (isGameOver) throw Error('Game ended');
|
||||
|
||||
setCard(nextCard);
|
||||
if (nextCard.gold) {
|
||||
if (goldCardCount < 3) {
|
||||
setGoldCardCount(goldCardCount + 1);
|
||||
} else {
|
||||
setGoldCardCount(0);
|
||||
setRound(round + 1);
|
||||
if (round === 5) alert('Good game!');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's bad**: Multiple re-renders (setCard -> setGoldCardCount -> setRound -> setIsGameOver). Also fragile for features like history replay.
|
||||
|
||||
---
|
||||
|
||||
## 6. Notifying Parent via Effect
|
||||
|
||||
```tsx
|
||||
// BAD: Effect to notify parent
|
||||
function Toggle({ onChange }) {
|
||||
const [isOn, setIsOn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(isOn);
|
||||
}, [isOn, onChange]);
|
||||
|
||||
function handleClick() {
|
||||
setIsOn(!isOn);
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Notify in same event
|
||||
function Toggle({ onChange }) {
|
||||
const [isOn, setIsOn] = useState(false);
|
||||
|
||||
function updateToggle(nextIsOn) {
|
||||
setIsOn(nextIsOn);
|
||||
onChange(nextIsOn); // Same event, batched render
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
updateToggle(!isOn);
|
||||
}
|
||||
}
|
||||
|
||||
// BEST: Fully controlled component
|
||||
function Toggle({ isOn, onChange }) {
|
||||
function handleClick() {
|
||||
onChange(!isOn);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Passing Data Up to Parent
|
||||
|
||||
```tsx
|
||||
// BAD: Child fetches, passes up via Effect
|
||||
function Parent() {
|
||||
const [data, setData] = useState(null);
|
||||
return <Child onFetched={setData} />;
|
||||
}
|
||||
|
||||
function Child({ onFetched }) {
|
||||
const data = useSomeAPI();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) onFetched(data);
|
||||
}, [onFetched, data]);
|
||||
}
|
||||
|
||||
// GOOD: Parent fetches, passes down
|
||||
function Parent() {
|
||||
const data = useSomeAPI();
|
||||
return <Child data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Data should flow down. Upward flow via Effects makes debugging hard.
|
||||
|
||||
---
|
||||
|
||||
## 8. Fetching Without Cleanup (Race Condition)
|
||||
|
||||
```tsx
|
||||
// BAD: No cleanup - race condition
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchResults(query).then(json => {
|
||||
setResults(json); // "hello" response may arrive after "hell"
|
||||
});
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// GOOD: Cleanup ignores stale responses
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
fetchResults(query).then(json => {
|
||||
if (!ignore) setResults(json);
|
||||
});
|
||||
|
||||
return () => { ignore = true; };
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. App Initialization in Effect
|
||||
|
||||
```tsx
|
||||
// BAD: Runs twice in dev, may break auth
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
loadDataFromLocalStorage();
|
||||
checkAuthToken(); // May invalidate token on second call!
|
||||
}, []);
|
||||
}
|
||||
|
||||
// GOOD: Module-level guard
|
||||
let didInit = false;
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
if (!didInit) {
|
||||
didInit = true;
|
||||
loadDataFromLocalStorage();
|
||||
checkAuthToken();
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ALSO GOOD: Module-level execution
|
||||
if (typeof window !== 'undefined') {
|
||||
checkAuthToken();
|
||||
loadDataFromLocalStorage();
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user