Back to articles
November 6, 2024
Avoid using useEffect #1 - Reset state on prop change
When building applications, we sometimes overlook bad practices or heavy calculations in our components—often simply because we’re unaware of the impact. Overusing useEffect can be one of the main reasons why our frontend application runs slower than expected and why the code becomes harder to read. You’ve likely encountered components with multiple, interdependent useEffect hooks that make the code difficult to follow.
Example: Checking prices in different currencies
There are our products:
const products = [ { id: 1, name: 't-shirt', price: 100, currency: 'PLN' }, { id: 2, name: 'trousers', price: 300, currency: 'PLN' }, { id: 3, name: 'jacket', price: 500, currency: 'PLN' } ]
We want to check prices in different currencies (they are read-only; we don’t intend to modify/edit data). We are creating two components. The first is our main component, which displays all products and allows us to select a product to view its price in different currencies.
const Products = () => { const [selectedProduct, setSelectedProduct] = useState(null); return ( <div> {products.map((product) => ( <div key={product.id}> <p>{product.name}</p> <p>{product.price}</p> <button onClick={() => setSelectedProduct(product)}>Set active product</button> </div> ))} {selectedProduct && ( <ProductPriceConverter productPrice={selectedProduct.price} productCurrency={selectedProduct.currency} /> )} </div> ); };
Once we select a product, our second component lets us choose a currency and converts the price from PLN to the selected currency.
const ProductPriceConverter = ({ productPrice, productCurrency }) => { const [price, setPrice] = useState(productPrice); const [currency, setCurrency] = useState(productCurrency); const changePriceBasedOnCurrency = (newCurrency) => { switch (newCurrency) { case 'PLN': setPrice(productPrice); break; case 'DKK': setPrice(productPrice * 1.71); break; case 'USD': setPrice(productPrice * 0.25); break; } }; const handleOnChangeCurrency = (event) => { setCurrency(event.target.value); changePriceBasedOnCurrency(event.target.value); }; return ( <div> <select defaultValue={currency} onChange={(event) => handleOnChangeCurrency(event)}> <option value="PLN">PLN</option> <option value="USD">USD</option> <option value="DKK">DKK</option> </select> <div> <p> Price in {currency}: {price} </p> </div> </div> ); };
If you test the code, it doesn’t work as expected. When we select the active product for the first time, we see that selectedProduct is correctly set to the first product we clicked on. However, if we try to change it, nothing happens—selectedProduct remains the first selected item. Why? This happens because we are passing the initial value to useState, which only sets the state once (on the initial render). When we change selectedProduct, our component ProductPriceConverter doesn’t recognize these changes. So, what can we do?
- We can add two useEffect hooks to this code to listen for prop changes. If new values are passed, we update the state accordingly.
const ProductPriceConverter = ({ productPrice, productCurrency }) => { const [price, setPrice] = useState(productPrice); const [currency, setCurrency] = useState(productCurrency); // Added two useEffects useEffect(() => { setPrice(productPrice); }, [productPrice]); useEffect(() => { setCurrency(productCurrency); }, [productCurrency]); const changePriceBasedOnCurrency = (newCurrency) => { switch (newCurrency) { case 'PLN': setPrice(productPrice); break; case 'DKK': setPrice(productPrice * 1.71); break; case 'USD': setPrice(productPrice * 0.25); break; } }; const handleOnChangeCurrency = (event) => { setCurrency(event.target.value); changePriceBasedOnCurrency(event.target.value); }; return ( <div> <select defaultValue={currency} onChange={(event) => handleOnChangeCurrency(event)}> <option value="PLN">PLN</option> <option value="USD">USD</option> <option value="DKK">DKK</option> </select> <div> <p> Price in {currency}: {price} </p> </div> </div> ); };
However, this approach isn’t very efficient. Using useEffect solely to update the state to keep it synchronized with the latest value isn’t ideal. Let’s explore a better solution to address this.
- Add a key where you call the component:
const Products = () => { const [selectedProduct, setSelectedProduct] = useState(null); return ( <div> {products.map((product) => ( <div key={product.id}> <p>{product.name}</p> <p>{product.price}</p> <button onClick={() => setSelectedProduct(product)}>Set active product</button> </div> ))} {selectedProduct && ( <ProductPriceConverter key={selectedProduct.id} // <== Here we are adding a key productPrice={selectedProduct.price} productCurrency={selectedProduct.currency} /> )} </div> ); };
Now you can see that by simply adding a key where we call the component, we no longer need to use useEffect directly in our component. With this approach, React treats the component as a new instance each time, resetting the state on every re-render thanks to the explicit key.
Summary: Avoiding useEffect for prop-based state updates
When building React applications, it’s easy to fall into performance issues or code readability problems by overusing useEffect. One common scenario is when we need to reset the state of a component based on prop changes, which can lead to excessive useEffect usage. In this example, we explored a cleaner and more efficient approach to synchronizing state with props.
Problem
In our example, we created two components:
Products: Displays a list of products and allows selecting one to view its price in different currencies.
ProductPriceConverter: Accepts a product price and currency as props, enabling users to select another currency and view the converted price. The initial solution attempted to use useEffect hooks in ProductPriceConverter to listen for changes in productPrice and productCurrency and update the state accordingly. However, this approach was inefficient and led to unnecessary complexity.
Solution
A more efficient approach was to pass a unique key to ProductPriceConverter in the Products component. By adding a key based on the selectedProduct.id, React treats ProductPriceConverter as a new instance each time the selected product changes. This forces React to re-render the component with fresh state, eliminating the need for useEffect to track prop changes.
Key Takeaways
- Avoid Overusing useEffect: Using useEffect solely to synchronize state with props can make code harder to read and maintain.
- Use Keys for Component Resets: Adding a unique key to a component forces React to treat it as a new instance, resetting its state without extra hooks.
- Cleaner Code and Improved Performance: This approach reduces unnecessary hooks and makes the component simpler and more performant.
- By using this technique, we can improve the efficiency and readability of our React components.