Updating state based on props or state
When something can be calculated from the existing props or state, don’t put it in state. Instead, calculate it during rendering
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // 🔴 Avoid: redundant state and unnecessary Effect const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(firstName + ' ' + lastName); }, [firstName, lastName]); // ... } Recommended
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // ✅ Good: calculated during rendering const fullName = firstName + ' ' + lastName; // ... } Caching expensive calculations
function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // 🔴 Avoid: redundant state and unnecessary Effect const [visibleTodos, setVisibleTodos] = useState([]); useEffect(() => { setVisibleTodos(getFilteredTodos(todos, filter)); }, [todos, filter]); // ... } Recommended
function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // ✅ This is fine if getFilteredTodos() is not slow. const visibleTodos = getFilteredTodos(todos, filter); // ... } Also Recommended
In many cases, the above code is fine! But maybe getFilteredTodos() is slow or you have a lot of todos. In that case you don’t want to recalculate getFilteredTodos() if some unrelated state variable like newTodo has changed.
You can cache (or “memoize”) an expensive calculation by wrapping it in a useMemo Hook:
import { useMemo, useState } from 'react'; function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // ✅ Does not re-run getFilteredTodos() unless todos or filter change const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]); // ... } Resetting all state when a prop changes
export default function ProfilePage({ userId }) { const [comment, setComment] = useState(''); // 🔴 Avoid: Resetting state on prop change in an Effect useEffect(() => { setComment(''); }, [userId]); // ... } This is inefficient because ProfilePage and its children will first render with the stale value, and then render again. It is also complicated because you’d need to do this in every component that has some state inside ProfilePage. For example, if the comment UI is nested, you’d want to clear out nested comment state too.
Instead, you can tell React that each user’s profile is conceptually a different profile by giving it an explicit key. Split your component in two and pass a key attribute from the outer component to the inner one:
Recommended
export default function ProfilePage({ userId }) { return ( <Profile userId={userId} key={userId} /> ); } function Profile({ userId }) { // ✅ This and any other state below will reset on key change automatically const [comment, setComment] = useState(''); // ... } Sharing logic between event handlers
When you’re not sure whether some code should be in an Effect or in an event handler, ask yourself why this code needs to run. Use Effects only for code that should run because the component was displayed to the user
function ProductPage({ product, addToCart }) { // 🔴 Avoid: Event-specific logic inside an Effect useEffect(() => { if (product.isInCart) { showNotification(`Added ${product.name} to the shopping cart!`); } }, [product]); function handleBuyClick() { addToCart(product); } function handleCheckoutClick() { addToCart(product); navigateTo('/checkout'); } // ... } Recommended
function ProductPage({ product, addToCart }) { // ✅ Good: Event-specific logic is called from event handlers function buyProduct() { addToCart(product); showNotification(`Added ${product.name} to the shopping cart!`); } function handleBuyClick() { buyProduct(); } function handleCheckoutClick() { buyProduct(); navigateTo('/checkout'); } // ... } Sending a POST request
function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); // ✅ Good: This logic should run because the component was displayed useEffect(() => { post('/analytics/event', { eventName: 'visit_form' }); }, []); // 🔴 Avoid: Event-specific logic inside an Effect const [jsonToSubmit, setJsonToSubmit] = useState(null); useEffect(() => { if (jsonToSubmit !== null) { post('/api/register', jsonToSubmit); } }, [jsonToSubmit]); function handleSubmit(e) { e.preventDefault(); setJsonToSubmit({ firstName, lastName }); } // ... } Recommended
function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); // ✅ Good: This logic runs because the component was displayed useEffect(() => { post('/analytics/event', { eventName: 'visit_form' }); }, []); function handleSubmit(e) { e.preventDefault(); // ✅ Good: Event-specific logic is in the event handler post('/api/register', { firstName, lastName }); } // ... } Chains of computations
function Game() { const [card, setCard] = useState(null); const [goldCardCount, setGoldCardCount] = useState(0); const [round, setRound] = useState(1); const [isGameOver, setIsGameOver] = useState(false); // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other useEffect(() => { if (card !== null && 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]); useEffect(() => { alert('Good game!'); }, [isGameOver]); function handlePlaceCard(nextCard) { if (isGameOver) { throw Error('Game already ended.'); } else { setCard(nextCard); } } // ... Recommended
function Game() { const [card, setCard] = useState(null); const [goldCardCount, setGoldCardCount] = useState(0); const [round, setRound] = useState(1); // ✅ Calculate what you can during rendering const isGameOver = round > 5; function handlePlaceCard(nextCard) { if (isGameOver) { throw Error('Game already ended.'); } // ✅ Calculate all the next state in the event handler setCard(nextCard); if (nextCard.gold) { if (goldCardCount <= 3) { setGoldCardCount(goldCardCount + 1); } else { setGoldCardCount(0); setRound(round + 1); if (round === 5) { alert('Good game!'); } } } } // ... Initializing the application
function App() { // 🔴 Avoid: Effects with logic that should only ever run once useEffect(() => { loadDataFromLocalStorage(); checkAuthToken(); }, []); // ... } Recommended
let didInit = false; function App() { useEffect(() => { if (!didInit) { didInit = true; // ✅ Only runs once per app load loadDataFromLocalStorage(); checkAuthToken(); } }, []); // ... } Passing data to the parent
function Parent() { const [data, setData] = useState(null); // ... return <Child onFetched={setData} />; } function Child({ onFetched }) { const data = useSomeAPI(); // 🔴 Avoid: Passing data to the parent in an Effect useEffect(() => { if (data) { onFetched(data); } }, [onFetched, data]); // ... } Recommended
function Parent() { const data = useSomeAPI(); // ... // ✅ Good: Passing data down to the child return <Child data={data} />; } function Child({ data }) { // ... } Subscribing to an external store
function useOnlineStatus() { // Not ideal: Manual store subscription in an Effect const [isOnline, setIsOnline] = useState(true); useEffect(() => { function updateState() { setIsOnline(navigator.onLine); } updateState(); window.addEventListener('online', updateState); window.addEventListener('offline', updateState); return () => { window.removeEventListener('online', updateState); window.removeEventListener('offline', updateState); }; }, []); return isOnline; } function ChatIndicator() { const isOnline = useOnlineStatus(); // ... } Recommended
Although it’s common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead. Delete the Effect and replace it with a call to useSyncExternalStore:
function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } function useOnlineStatus() { // ✅ Good: Subscribing to an external store with a built-in Hook return useSyncExternalStore( subscribe, // React won't resubscribe for as long as you pass the same function () => navigator.onLine, // How to get the value on the client () => true // How to get the value on the server ); } function ChatIndicator() { const isOnline = useOnlineStatus(); // ... } Fetching data
function SearchResults({ query }) { const [results, setResults] = useState([]); const [page, setPage] = useState(1); useEffect(() => { // 🔴 Avoid: Fetching without cleanup logic fetchResults(query, page).then(json => { setResults(json); }); }, [query, page]); function handleNextPageClick() { setPage(page + 1); } // ... } Recommended
function SearchResults({ query }) { const [results, setResults] = useState([]); const [page, setPage] = useState(1); useEffect(() => { let ignore = false; fetchResults(query, page).then(json => { if (!ignore) { setResults(json); } }); return () => { ignore = true; }; }, [query, page]); function handleNextPageClick() { setPage(page + 1); } // ... } **Note: Examples are referred from https://beta.reactjs.org/learn/you-might-not-need-an-effect
Top comments (0)