My Journey with React Hooks: From Class Components to Modern React
When I first started learning React, class components were the standard way to manage state and lifecycle methods. They seemed logical at the time—you have a class, methods, and state. But as I built more complex applications, I noticed something: my code was becoming verbose, difficult to test, and hard to maintain.
The Breaking Point
The moment I knew I had to learn hooks was during the development of my entertainment hub application. I had a component with over 15 different methods, nested this.setState calls, and a componentDidMount that was 50 lines long. The component was doing too much, and splitting it up seemed impossible because the logic was so tightly coupled.
Starting with useState: The First Revelation
The first hook I learned was useState, and honestly, it blew my mind. Compare these two approaches:
Before (Class Component):
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
loading: false
};
}
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleIncrement}>Increment</button>
</div>
);
}
}
After (Hooks):
function Counter() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const handleIncrement = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
The difference was immediately clear. Less boilerplate, more readable, and I could focus on the logic rather than the ceremony of class components.
useEffect: Understanding the Lifecycle
Understanding useEffect took me longer. I kept thinking of it as componentDidMount, but it's so much more powerful. The dependency array was confusing at first, but once I understood it, I could control exactly when my effects ran.
Here's what I learned about useEffect:
Data Fetching:
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []); // Empty dependency array means run once on mount
Cleanup:
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval); // Cleanup function
}, []);
Custom Hooks: My First "Aha" Moment
Creating my first custom hook felt like magic. I had repetitive data fetching logic across multiple components, and suddenly, complex logic became reusable. Here's the custom hook I created:
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
I used this hook in 5 different components, eliminating hundreds of lines of duplicate code!
What I Wish I Knew Earlier
After working with hooks for over a year, here's what I wish someone had told me when I started:
1. Don't Overthink the Dependency Array
Include what you use, and let ESLint help you. The exhaustive-deps rule is your friend, not your enemy.
2. Custom Hooks Aren't Scary
If you're repeating logic across components, extract it into a custom hook. Start simple—even a hook that just manages a boolean state is valuable.
3. useState Doesn't Merge Objects
Unlike this.setState in class components, useState completely replaces the state. Use the spread operator or multiple state variables:
// Don't do this
const [user, setUser] = useState({ name: '', email: '' });
setUser({ name: 'John' }); // This loses the email!
// Do this instead
setUser(prev => ({ ...prev, name: 'John' }));
// Or use separate state variables
const [name, setName] = useState('');
const [email, setEmail] = useState('');
4. useCallback and useMemo Aren't Always Needed
Start simple, optimize later. These hooks are for performance optimization, not for every function or calculation.
The Real-World Impact
After switching to hooks, my code became cleaner, easier to test, and more enjoyable to write. My components went from 100+ line classes to 20-30 line functions. More importantly, my applications became more maintainable.
Here's a real example from my movie app:
Before: A 120-line MovieDetail class component with complex state management
After: A 35-line functional component with three custom hooks (useMovieDetails, useWatchlist, useRecommendations)
The hooks approach made each piece of logic focused and reusable. I could easily test each hook independently and reuse the logic in other components.
Tips for Learning Hooks
Looking Forward
React Hooks changed how I think about component design. Instead of thinking about lifecycle methods, I think about synchronizing with external systems. Instead of complex class hierarchies, I compose simple functions.
If you're still using class components, I encourage you to try hooks. Start with a simple component, and I guarantee you'll be amazed at how much cleaner your code becomes.
The learning curve is worth it, and once you understand hooks, you'll wonder how you ever lived without them.