Back to articles
December 11, 2024
Avoid using useEffect #2 - Update state based on props or state
See related article: https://www.frontendforge.dev/blog/avoid-using-useeffect-reset-state-on-prop-change
In the previous article, we explored how to avoid overusing useEffect when managing state resets based on prop changes. Now, let’s dive into another common scenario: updating state based on other states or props. While it may seem intuitive to reach for useEffect in these cases, there’s often a simpler and more efficient way.
Example: Calculating Final Price Based on Price and Currency
Let’s consider a component that calculates the final price based on price and currency. Here’s how it looks when we use useEffect to manage this derived state:
const ComponentWithUseEffect = () => { const [currency, setCurrency] = useState('PLN'); const [price, setPrice] = useState(0); const [finalPrice, setFinalPrice] = useState(''); useEffect(() => { setFinalPrice(`${price} ${currency}`) }, [currency, price]) return ( <> <input value={price} onChange={event => setPrice(Number(event.target.value))} /> <select defaultValue={currency} onChange={(event) => setCurrency(event.target.value)}> <option value="PLN">PLN</option> <option value="USD">USD</option> <option value="DKK">DKK</option> </select> <p>Final Price: {finalPrice}</p> </> ) }
At first glance, this seems fine. We’re listening for changes in price or currency and updating finalPrice accordingly. But this approach introduces unnecessary complexity:
- Extra State Management: We’re adding a state variable, finalPrice, that can be derived directly from existing states (price and currency).
- Additional Rendering: The setFinalPrice call inside useEffect triggers a second render every time price or currency changes.
- Readability Issues: The use of useEffect here makes the logic harder to follow, especially in more complex components.
A Simpler Solution: Derive Values During Render
Instead of using useEffect and extra state, we can calculate the final price directly in the render phase:
const ComponentWithoutUseEffect = () => { const [currency, setCurrency] = useState('PLN'); const [price, setPrice] = useState(0); const finalPrice = `${price} ${currency}` return ( <> <input value={price} onChange={event => setPrice(Number(event.target.value))} /> <select defaultValue={currency} onChange={(event) => setCurrency(event.target.value)}> <option value="PLN">PLN</option> <option value="USD">USD</option> <option value="DKK">DKK</option> </select> <p>Final Price: {finalPrice}</p> </> ) }
In this version, we simply calculate finalPrice as part of the render process. There’s no extra state or useEffect to manage. This approach has several advantages:
- No Extra State: finalPrice is a derived value, not state. This reduces state management overhead.
- Fewer Renders: Since there’s no setState call, React renders the component only once per change.
- Improved Readability: The logic is straightforward and directly tied to the component’s render output.
Why This Approach Works Better
React’s rendering mechanism already recalculates the component tree whenever state or props change. By deriving values like finalPrice directly in the render function, we align with React’s natural lifecycle, avoiding unnecessary renders and improving performance.
Here’s a quick comparison:
Aspect | With useEffect | Without useEffect |
---|---|---|
Code Simplicity | Requires extra state and logic for derived values. | Derived values calculated inline, no extra logic. |
Performance | Causes an additional render due to setState in useEffect . | Single render, as calculations occur during render. |
Readability | Harder to follow with interdependent states and effects. | Cleaner and easier to understand. |
Use Case | Needed only if the calculation has side effects. | Ideal for pure computations. |
Summary: Avoiding useEffect for Derived State Updates
When building React applications, it’s common to derive state based on existing state or props. While useEffect may seem like an intuitive solution, it often introduces unnecessary complexity, extra renders, and reduced readability. By calculating derived values inline during the render phase, you can simplify your components and improve performance.
In this article, we compared two approaches for calculating a final price based on state. The first approach relied on useEffect to manage derived state, adding an extra state variable and triggering multiple renders. The second approach avoided useEffect altogether, directly calculating the derived value during render. This resulted in cleaner, more efficient code.
Key Takeaways:
- Avoid useEffect for purely derived state; calculate values inline during render instead.
- Inline calculations reduce unnecessary renders and simplify state management.
- Use useEffect only for actual side effects, synchronize data or API calls. However, for API calls the best approach is to use useEffect only for the initial API call. When possible, API calls should be made directly in handlers (an article about this is coming soon...).
- Cleaner, simpler components are easier to read, maintain, and debug.
- By aligning with React’s natural rendering lifecycle, you can write more efficient and readable components, avoiding the pitfalls of overusing useEffect.