Combining useState and useEffect

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 useState and useEffect.
  • 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:

  • useState manages the count variable.
  • useEffect logs the count whenever it changes.
  • The dependency array [count] ensures the effect runs only when count updates.

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 (
&lt;ul&gt;
  {users.map(user =&gt; (
    &lt;li key={user.id}&gt;{user.name} ({user.email})&lt;/li&gt;
  ))}
&lt;/ul&gt;
); }

Explanation:

  • users state stores API data.
  • loading state controls the UI while fetching.
  • useEffect runs 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 =&gt;
  item.toLowerCase().includes(search.toLowerCase())
));
}, [search, items]); return (
&lt;div&gt;
  &lt;input 
    type="text" 
    placeholder="Search..." 
    value={search} 
    onChange={e =&gt; setSearch(e.target.value)} 
  /&gt;
  &lt;ul&gt;
    {filteredItems.map((item, index) =&gt; &lt;li key={index}&gt;{item}&lt;/li&gt;)}
  &lt;/ul&gt;
&lt;/div&gt;
); }

Key Points:

  • The search state updates dynamically.
  • useEffect recalculates filteredItems whenever search changes.
  • 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) : &#91;];
}); const [input, setInput] = useState(""); useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]); const addTodo = () => {
if (input.trim() === "") return;
setTodos(prev =&gt; &#91;...prev, input]);
setInput("");
}; const removeTodo = (index) => {
setTodos(prev =&gt; prev.filter((_, i) =&gt; i !== index));
}; return (
&lt;div&gt;
  &lt;h2&gt;Todo List&lt;/h2&gt;
  &lt;input 
    value={input} 
    onChange={e =&gt; setInput(e.target.value)} 
    placeholder="Add a todo" 
  /&gt;
  &lt;button onClick={addTodo}&gt;Add&lt;/button&gt;
  &lt;ul&gt;
    {todos.map((todo, index) =&gt; (
      &lt;li key={index}&gt;
        {todo} 
        &lt;button onClick={() =&gt; removeTodo(index)}&gt;Delete&lt;/button&gt;
      &lt;/li&gt;
    ))}
  &lt;/ul&gt;
&lt;/div&gt;
); }

Explanation:

  • State is initialized from localStorage if available.
  • useEffect updates localStorage whenever todos changes.
  • 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 =&gt; res.json())
  .then(data =&gt; setPosts(data));
}, [userId]); // Effect runs when userId changes return (
&lt;div&gt;
  &lt;h3&gt;Posts for User {userId}&lt;/h3&gt;
  &lt;ul&gt;
    {posts.map(post =&gt; &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;)}
  &lt;/ul&gt;
&lt;/div&gt;
); }
  • The effect re-runs whenever userId changes.
  • 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(() =&gt; setSeconds(prev =&gt; prev + 1), 1000);
return () =&gt; 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 =&gt; res.json())
  .then(data =&gt; setUsers(data));
}, []); useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
  .then(res =&gt; res.json())
  .then(data =&gt; setPosts(data));
}, []); return (
&lt;div&gt;
  &lt;h2&gt;Users&lt;/h2&gt;
  &lt;ul&gt;{users.map(u =&gt; &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;)}&lt;/ul&gt;
  &lt;h2&gt;Posts&lt;/h2&gt;
  &lt;ul&gt;{posts.map(p =&gt; &lt;li key={p.id}&gt;{p.title}&lt;/li&gt;)}&lt;/ul&gt;
&lt;/div&gt;
); }
  • Each effect handles a separate concern.
  • Avoids mixing unrelated logic in a single effect.

Best Practices

  1. Use dependency arrays correctly to prevent unnecessary renders.
  2. Use functional updates with useState when new state depends on previous state.
  3. Initialize state from storage if persistence is required.
  4. Separate concerns by using multiple useEffect hooks.
  5. Cleanup side effects to prevent memory leaks.
  6. Avoid unnecessary API calls by controlling dependencies.
  7. Keep effects lightweight; complex logic can be moved to helper functions.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *