react-best-practices

When working with React, always load both this skill and typescript-best-practices together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-best-practices" with this command: npx skills add neversight/skills_feed/neversight-skills-feed-react-best-practices

React Best Practices

Pair with TypeScript

When working with React, always load both this skill and typescript-best-practices together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.

Core Principle: Effects Are Escape Hatches

Effects let you "step outside" React to synchronize with external systems. Most component logic should NOT use Effects. Before writing an Effect, ask: "Is there a way to do this without an Effect?"

When to Use Effects

Effects are for synchronizing with external systems:

  • Subscribing to browser APIs (WebSocket, IntersectionObserver, resize)

  • Connecting to third-party libraries not written in React

  • Setting up/cleaning up event listeners on window/document

  • Fetching data on mount (though prefer React Query or framework data fetching)

  • Controlling non-React DOM elements (video players, maps, modals)

When NOT to Use Effects

Derived State (Calculate During Render)

// BAD: Effect for derived state const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(firstName + ' ' + lastName); }, [firstName, lastName]);

// GOOD: Calculate during render const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); const fullName = firstName + ' ' + lastName;

Expensive Calculations (Use useMemo)

// BAD: Effect for caching const [visibleTodos, setVisibleTodos] = useState([]); useEffect(() => { setVisibleTodos(getFilteredTodos(todos, filter)); }, [todos, filter]);

// GOOD: useMemo for expensive calculations const visibleTodos = useMemo( () => getFilteredTodos(todos, filter), [todos, filter] );

Resetting State on Prop Change (Use key)

// BAD: Effect to reset state function ProfilePage({ userId }) { const [comment, setComment] = useState(''); useEffect(() => { setComment(''); }, [userId]); // ... }

// GOOD: Use key to reset component state function ProfilePage({ userId }) { return <Profile userId={userId} key={userId} />; }

function Profile({ userId }) { const [comment, setComment] = useState(''); // Resets automatically // ... }

User Event Handling (Use Event Handlers)

// BAD: Event-specific logic in Effect function ProductPage({ product, addToCart }) { useEffect(() => { if (product.isInCart) { showNotification(Added ${product.name} to cart); } }, [product]); // ... }

// GOOD: Logic in event handler function ProductPage({ product, addToCart }) { function buyProduct() { addToCart(product); showNotification(Added ${product.name} to cart); } // ... }

Notifying Parent of State Changes

// BAD: Effect to notify parent function Toggle({ onChange }) { const [isOn, setIsOn] = useState(false); useEffect(() => { onChange(isOn); }, [isOn, onChange]); // ... }

// GOOD: Update both in event handler function Toggle({ onChange }) { const [isOn, setIsOn] = useState(false); function updateToggle(nextIsOn) { setIsOn(nextIsOn); onChange(nextIsOn); } // ... }

// BEST: Fully controlled component function Toggle({ isOn, onChange }) { function handleClick() { onChange(!isOn); } // ... }

Chains of Effects

// BAD: Effect chain useEffect(() => { if (card !== null && card.gold) { setGoldCardCount(c => c + 1); } }, [card]);

useEffect(() => { if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0); } }, [goldCardCount]);

// GOOD: Calculate derived state, update in event handler const isGameOver = round > 5;

function handlePlaceCard(nextCard) { setCard(nextCard); if (nextCard.gold) { if (goldCardCount < 3) { setGoldCardCount(goldCardCount + 1); } else { setGoldCardCount(0); setRound(round + 1); } } }

Effect Dependencies

Never Suppress the Linter

// BAD: Suppressing linter hides bugs useEffect(() => { const id = setInterval(() => { setCount(count + increment); }, 1000); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []);

// GOOD: Fix the code, not the linter useEffect(() => { const id = setInterval(() => { setCount(c => c + increment); }, 1000); return () => clearInterval(id); }, [increment]);

Use Updater Functions to Remove State Dependencies

// BAD: messages in dependencies causes reconnection on every message useEffect(() => { connection.on('message', (msg) => { setMessages([...messages, msg]); }); // ... }, [messages]); // Reconnects on every message!

// GOOD: Updater function removes dependency useEffect(() => { connection.on('message', (msg) => { setMessages(msgs => [...msgs, msg]); }); // ... }, []); // No messages dependency needed

Move Objects/Functions Inside Effects

// BAD: Object created each render triggers Effect function ChatRoom({ roomId }) { const options = { serverUrl, roomId }; // New object each render useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); // Reconnects every render! }

// GOOD: Create object inside Effect function ChatRoom({ roomId }) { useEffect(() => { const options = { serverUrl, roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId, serverUrl]); // Only reconnects when values change }

useEffectEvent for Non-Reactive Logic

// BAD: theme change reconnects chat function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); // Reconnects on theme change! }

// GOOD: useEffectEvent for non-reactive logic function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); });

useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); // theme no longer causes reconnection }

Wrap Callback Props with useEffectEvent

// BAD: Callback prop in dependencies function ChatRoom({ roomId, onReceiveMessage }) { useEffect(() => { connection.on('message', onReceiveMessage); // ... }, [roomId, onReceiveMessage]); // Reconnects if parent re-renders }

// GOOD: Wrap callback in useEffectEvent function ChatRoom({ roomId, onReceiveMessage }) { const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => { connection.on('message', onMessage); // ... }, [roomId]); // Stable dependency list }

Effect Cleanup

Always Clean Up Subscriptions

useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); // REQUIRED }, [roomId]);

useEffect(() => { function handleScroll(e) { console.log(window.scrollY); } window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); // REQUIRED }, []);

Data Fetching with Ignore Flag

useEffect(() => { let ignore = false;

async function fetchData() { const result = await fetchTodos(userId); if (!ignore) { setTodos(result); } }

fetchData();

return () => { ignore = true; // Prevents stale data from old requests }; }, [userId]);

Development Double-Fire Is Intentional

React remounts components in development to verify cleanup works. If you see effects firing twice, don't try to prevent it with refs:

// BAD: Hiding the symptom const didInit = useRef(false); useEffect(() => { if (didInit.current) return; didInit.current = true; // ... }, []);

// GOOD: Fix the cleanup useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); // Proper cleanup }, []);

Refs

Use Refs for Values That Don't Affect Rendering

// GOOD: Ref for timeout ID (doesn't affect UI) const timeoutRef = useRef(null);

function handleClick() { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { // ... }, 1000); }

// BAD: Using ref for displayed value const countRef = useRef(0); countRef.current++; // UI won't update!

Never Read/Write ref.current During Render

// BAD: Reading ref during render function MyComponent() { const ref = useRef(0); ref.current++; // Mutating during render! return <div>{ref.current}</div>; // Reading during render! }

// GOOD: Read/write refs in event handlers and effects function MyComponent() { const ref = useRef(0);

function handleClick() { ref.current++; // OK in event handler }

useEffect(() => { ref.current = someValue; // OK in effect }, [someValue]); }

Ref Callbacks for Dynamic Lists

// BAD: Can't call useRef in a loop {items.map((item) => { const ref = useRef(null); // Rule violation! return <li ref={ref} />; })}

// GOOD: Ref callback with Map const itemsRef = useRef(new Map());

{items.map((item) => ( <li key={item.id} ref={(node) => { if (node) { itemsRef.current.set(item.id, node); } else { itemsRef.current.delete(item.id); } }} /> ))}

useImperativeHandle for Controlled Exposure

// Limit what parent can access function MyInput({ ref }) { const realInputRef = useRef(null);

useImperativeHandle(ref, () => ({ focus() { realInputRef.current.focus(); }, // Parent can ONLY call focus(), not access full DOM node }));

return <input ref={realInputRef} />; }

Custom Hooks

Hooks Share Logic, Not State

// Each call gets independent state function StatusBar() { const isOnline = useOnlineStatus(); // Own state }

function SaveButton() { const isOnline = useOnlineStatus(); // Separate state instance }

Name Hooks useXxx Only If They Use Hooks

// BAD: useXxx but doesn't use hooks function useSorted(items) { return items.slice().sort(); }

// GOOD: Regular function function getSorted(items) { return items.slice().sort(); }

// GOOD: Uses hooks, so prefix with use function useAuth() { return useContext(AuthContext); }

Avoid "Lifecycle" Hooks

// BAD: Custom lifecycle hooks function useMount(fn) { useEffect(() => { fn(); }, []); // Missing dependency, linter can't catch it }

// GOOD: Use useEffect directly useEffect(() => { doSomething(); }, [doSomething]);

Keep Custom Hooks Focused

// GOOD: Focused, concrete use cases useChatRoom({ serverUrl, roomId }); useOnlineStatus(); useFormInput(initialValue);

// BAD: Generic, abstract hooks useMount(fn); useEffectOnce(fn); useUpdateEffect(fn);

Component Patterns

Controlled vs Uncontrolled

// Uncontrolled: component owns state function SearchInput() { const [query, setQuery] = useState(''); return <input value={query} onChange={e => setQuery(e.target.value)} />; }

// Controlled: parent owns state function SearchInput({ query, onQueryChange }) { return <input value={query} onChange={e => onQueryChange(e.target.value)} />; }

Prefer Composition Over Prop Drilling

// BAD: Prop drilling <App user={user}> <Layout user={user}> <Header user={user}> <Avatar user={user} /> </Header> </Layout> </App>

// GOOD: Composition with children <App> <Layout> <Header avatar={<Avatar user={user} />} /> </Layout> </App>

// GOOD: Context for truly global state <UserContext.Provider value={user}> <App /> </UserContext.Provider>

flushSync for Synchronous DOM Updates

// When you need to read DOM immediately after state update import { flushSync } from 'react-dom';

function handleAdd() { flushSync(() => { setTodos([...todos, newTodo]); }); // DOM is now updated, safe to read listRef.current.lastChild.scrollIntoView(); }

Summary: Decision Tree

  • Need to respond to user interaction? Use event handler

  • Need computed value from props/state? Calculate during render

  • Need cached expensive calculation? Use useMemo

  • Need to reset state on prop change? Use key prop

  • Need to synchronize with external system? Use Effect with cleanup

  • Need non-reactive code in Effect? Use useEffectEvent

  • Need mutable value that doesn't trigger render? Use ref

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-env

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

_skillwriting

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review