Introduction
In React, building dynamic applications often requires both state management and side effects. The useState hook provides the ability to manage state in functional components, while useEffect allows you to handle side effects like fetching data, subscribing to events, or interacting with browser APIs.
Combining useState and useEffect enables developers to create dynamic, reactive UIs that respond to data changes and external events efficiently.
In this post, we will explore:
- Fetching data from APIs using
useStateanduseEffect. - Updating the UI dynamically based on state changes.
- A practical example: A Todo list with local storage persistence.
Understanding useEffect
useEffect is a React Hook that runs side effects after the component renders. Unlike state updates, side effects interact with the external world.
Syntax
useEffect(() => {
// Side effect logic here
}, [dependencies]);
- The first argument is a function containing the effect logic.
- The second argument is an array of dependencies; the effect runs whenever these values change.
- If the dependency array is empty
[], the effect runs only once after the initial render.
Basic Example: Logging State Changes
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count has changed:", count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
Explanation:
useStatemanages thecountvariable.useEffectlogs the count whenever it changes.- The dependency array
[count]ensures the effect runs only whencountupdates.
Fetching Data from APIs
A common use case is fetching data from an API when a component mounts. useEffect combined with useState makes this straightforward.
Example: Fetching Users
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(error => console.error("Error fetching users:", error));
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}
Explanation:
usersstate stores API data.loadingstate controls the UI while fetching.useEffectruns once after the component mounts.
Dynamic UI Updates Based on State Changes
useEffect allows UI updates that depend on state. For instance, filtering a list based on a search term:
function FilterableList() {
const [items] = useState(["Apple", "Banana", "Cherry", "Date"]);
const [search, setSearch] = useState("");
const [filteredItems, setFilteredItems] = useState(items);
useEffect(() => {
setFilteredItems(items.filter(item =>
item.toLowerCase().includes(search.toLowerCase())
));
}, [search, items]);
return (
<div>
<input
type="text"
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<ul>
{filteredItems.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
);
}
Key Points:
- The
searchstate updates dynamically. useEffectrecalculatesfilteredItemswheneversearchchanges.- This approach keeps the UI in sync with user input.
Persisting State with Local Storage
Combining useState and useEffect allows persistence of state using browser storage, such as localStorage.
Example: Todo List with Local Storage
function TodoApp() {
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem("todos");
return savedTodos ? JSON.parse(savedTodos) : [];
});
const [input, setInput] = useState("");
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (input.trim() === "") return;
setTodos(prev => [...prev, input]);
setInput("");
};
const removeTodo = (index) => {
setTodos(prev => prev.filter((_, i) => i !== index));
};
return (
<div>
<h2>Todo List</h2>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Explanation:
- State is initialized from
localStorageif available. useEffectupdateslocalStoragewhenevertodoschanges.- The UI updates dynamically, reflecting additions and deletions.
Handling Side Effects with Dependencies
useEffect can run effects conditionally based on dependencies. Understanding dependency management is crucial to avoid unnecessary renders or infinite loops.
Example: Dependent API Call
function PostList({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(https://jsonplaceholder.typicode.com/posts?userId=${userId})
.then(res => res.json())
.then(data => setPosts(data));
}, [userId]); // Effect runs when userId changes
return (
<div>
<h3>Posts for User {userId}</h3>
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
- The effect re-runs whenever
userIdchanges. - This enables dynamic fetching based on user interaction.
Cleanup Effects
useEffect can return a function to clean up side effects, such as subscriptions or timers.
Example: Timer
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => setSeconds(prev => prev + 1), 1000);
return () => clearInterval(interval); // Cleanup on unmount
}, []);
return <p>Seconds: {seconds}</p>;
}
Explanation:
- Timer increments every second.
- Cleanup prevents memory leaks when the component unmounts.
Combining Multiple Effects
You can use multiple useEffect hooks in the same component for separation of concerns.
function Dashboard() {
const [users, setUsers] = useState([]);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(data => setUsers(data));
}, []);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(res => res.json())
.then(data => setPosts(data));
}, []);
return (
<div>
<h2>Users</h2>
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
<h2>Posts</h2>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
- Each effect handles a separate concern.
- Avoids mixing unrelated logic in a single effect.
Best Practices
- Use dependency arrays correctly to prevent unnecessary renders.
- Use functional updates with
useStatewhen new state depends on previous state. - Initialize state from storage if persistence is required.
- Separate concerns by using multiple
useEffecthooks. - Cleanup side effects to prevent memory leaks.
- Avoid unnecessary API calls by controlling dependencies.
- Keep effects lightweight; complex logic can be moved to helper functions.
Leave a Reply