Why You Might Not Need an Effect: Optimizing React Components

Darshana Prachi
3 min readMay 10, 2024

--

In modern React development, the useEffect Hook is a powerful tool. However, it’s often overused or misused, leading to unnecessary complexity and potential performance issues. Instead of always reaching for useEffect, there are alternatives worth considering. This article explores those alternatives and provides practical coding examples.

Common Misuse Cases

1. Synchronizing State

A common use case is synchronizing state variables. But React’s onChange and other event handlers can handle most state updates directly.

Incorrect Example:

import { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);
const [double, setDouble] = useState(0);

useEffect(() => {
setDouble(count * 2); // Side effect to compute double
}, [count]);

return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
<p>Double: {double}</p>
</div>
);
}

Optimized Solution:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const double = count * 2; // Compute directly without useEffect

return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
<p>Double: {double}</p>
</div>
);
}

2. Fetching Data

Fetching data in useEffect is necessary but can often be simplified by using a data-fetching library like react-query or even React's new Server Components.

Incorrect Example:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
async function fetchProfile() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setProfile(data);
}
fetchProfile();
}, [userId]);
return profile ? <div>{profile.name}</div> : <p>Loading...</p>;
}

Optimized Solution with React Server Components:

// components/UserProfile.server.js
import React from 'react';

export default async function UserProfile({ userId }) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const profile = await response.json();

return <div>{profile.name}</div>;
}

Usage in the Client Component:

// components/App.client.js
import UserProfile from './UserProfile.server';

function App({ userId }) {
return (
<div>
<h1>User Profile</h1>
<UserProfile userId={userId} />
</div>
);
}

export default App;

3. Form Inputs and Debounced Updates

Instead of debouncing updates through useEffect, handle it directly via controlled input components.

Incorrect Example:

import { useState, useEffect } from 'react';

function Search() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState(query);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Optimized Solution with a Custom Hook:

import { useState } from 'react';
import useDebounce from './useDebounce';

function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

The useDebounce custom Hook would encapsulate the debouncing logic:

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;

4. Resetting All State When a Prop Changes

When navigating between different items, resetting state variables like form fields or filters is important. An inefficient way to handle this reset is by using the useEffect hook to manually clear the state based on the prop change. However, this results in an extra render cycle and leads to unnecessary complexity.

Incorrect Example:

import { useState, useEffect } from 'react';

export default function ItemDetails({ itemId }) {
const [input, setInput] = useState('');
// 🔴 Inefficient: Resets state with an effect
useEffect(() => {
setInput(''); // Clear the input field when itemId changes
}, [itemId]);

return <input value={input} onChange={(e) => setInput(e.target.value)} />;
}

Instead of relying on effects, we can use the key prop to trigger a reset automatically. By splitting the component into two and passing a unique key prop to the inner component, React will treat it as a new instance, ensuring all internal state is reset.

Optimized Solution:

export default function ItemDetails({ itemId }) {
return <ItemForm itemId={itemId} key={itemId} />;
}

function ItemForm({ itemId }) {
// ✅ State resets on key change automatically
const [input, setInput] = useState('');
return (
<div>
<h2>Details for Item {itemId}</h2>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</div>
);
}

Conclusion

React’s useEffect is valuable but often misapplied. In many cases, using alternative approaches like event handlers, libraries like react-query, or custom hooks can provide cleaner, more maintainable solutions. For more insights, check out React's guide.

--

--