.agents/skills/react-useeffect/anti-patterns.md
// 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.
// 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]
);
}
// 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.
// 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.
// 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.
// 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);
}
}
// 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.
// 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]);
}
// 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();
}