Hooks - useState and useEffect
What are Hooks?
Hooks are functions that let functional components "hook into" React features like state and lifecycle methods. Before hooks (introduced in React 16.8), these features were only available in class components. Hooks are now the standard way to write React.
The two most important hooks are useState and useEffect.
useState
useState adds state to a functional component. It returns an array with two items: the current value and a setter function.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // Initial value: 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Rules for useState
- Always call hooks at the top level of your component — never inside loops, conditions, or nested functions
- The setter function (
setCount) replaces the old state with the new value - React re-renders the component every time the setter is called with a different value
Multiple State Variables
Each useState call manages one piece of state:
function UserForm() {
const [name, setName] = useState("");
const [age, setAge] = useState(18);
const [isSubmitted, setIsSubmitted] = useState(false);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input type="number" value={age} onChange={e => setAge(Number(e.target.value))} />
<button type="submit">Submit</button>
</form>
);
}
State with Objects and Arrays
const [user, setUser] = useState({ name: "", email: "" });
// Wrong — mutating state directly
user.name = "Amara"; // Never do this!
// Correct — create a new object using spread
setUser({ ...user, name: "Amara" });
// Arrays — use spread or methods that return new arrays
const [items, setItems] = useState([]);
setItems([...items, newItem]); // Add
setItems(items.filter(i => i.id !== id)); // Remove
useEffect
useEffect runs side effects — code that happens outside of rendering. Common uses include fetching data, setting up event listeners, updating the document title, and setting timers.
import { useState, useEffect } from "react";
function DocumentTitleUpdater({ title }) {
useEffect(() => {
document.title = title; // Side effect: update browser tab title
}); // Runs after every render
return <h1>{title}</h1>;
}
The Dependency Array
The second argument to useEffect controls when it runs:
// No dependency array — runs after every render
useEffect(() => { ... });
// Empty array — runs once on mount (like componentDidMount)
useEffect(() => {
console.log("Component mounted");
}, []);
// With dependencies — runs when those values change
useEffect(() => {
console.log("userId changed:", userId);
fetchUser(userId);
}, [userId]);
Fetching Data with useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <p>{user?.name}</p>;
}
Cleanup Functions
Some effects need to be cleaned up to avoid memory leaks:
useEffect(() => {
const timer = setInterval(() => {
console.log("Tick");
}, 1000);
// Return a cleanup function
return () => {
clearInterval(timer); // Runs when component unmounts or before re-running the effect
};
}, []);
Common Patterns
Toggle State
const [isOpen, setIsOpen] = useState(false);
<button onClick={() => setIsOpen(prev => !prev)}>
{isOpen ? "Close" : "Open"}
</button>
Using the functional update form (prev => !prev) is safer when the new state depends on the old one, because React may batch state updates.
Key Takeaways
- Hooks let functional components use React features like state and lifecycle; always call them at the top level
- useState(initialValue) returns [currentValue, setter]; calling the setter triggers a re-render
- Never mutate state directly — use the setter with a new value; use spread (...) to update objects and arrays
- useEffect runs side effects after rendering; the dependency array controls when it re-runs
- An empty dependency array [] means the effect runs once on mount; omitting it means it runs after every render
- Return a cleanup function from useEffect to clear timers, cancel requests, or remove listeners
Practice Exercise
- Build a
Timercomponent that counts seconds usinguseStateandsetIntervalinsideuseEffectwith a cleanup function - Build a
DataFetchercomponent that fetches posts fromhttps://jsonplaceholder.typicode.com/posts?_limit=5and shows a loading state while fetching - Add a button to your timer to pause and resume counting
Try it yourself
Key Takeaways
- useState(initialValue) returns [value, setter]; calling the setter with a new value triggers a re-render
- Always call hooks at the top level of your component, never inside loops or conditions
- Never mutate state directly — use the setter with a new value; use spread (...) to update objects and arrays immutably
- useEffect runs after rendering; use the dependency array to control when it re-runs
- An empty dependency array ([]) runs the effect once on mount; specific dependencies run it when they change
- Return a cleanup function from useEffect to clear timers, cancel subscriptions, or remove event listeners
Quick Quiz
1.What does useState(0) return?
2.What does an empty dependency array ([]) mean in useEffect?
3.Why should you return a cleanup function from useEffect when using setInterval?
4.When updating an object in state, why must you use spread syntax?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx