DEV Community

Safal Bhandari
Safal Bhandari

Posted on

Building a Smarter Custom Hook in React: Auto-Refreshing Data with Cleanup

React’s custom hooks are a powerful way to organize reusable logic. In this article, we’ll create a custom hook called useTodos that not only fetches todos but also auto-refreshes them at a given interval.

We’ll also highlight one of the trickiest parts of useEffect: dependencies.


Why Use a Custom Hook Instead of Fetching in App?

If we fetched todos directly in App:

  • The component would mix UI logic and data-fetching logic, making it harder to read.
  • Reusing the fetching logic in another component would mean copy-pasting code.
  • Managing intervals and cleanup would clutter the component.

By moving the fetching + interval logic into a custom hook, we get:
✔ Reusability
✔ Cleaner code
✔ Automatic cleanup
✔ Better separation of concerns


The Custom Hook with Interval Refresh

import { useEffect, useState } from "react";
import axios from "axios";

function useTodos(timeout) {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Function to fetch todos
    const fetchTodos = () => {
      axios.get("https://dummyjson.com/todos").then((res) => {
        setTodos(res.data.todos);
        setLoading(false);
      });
    };

    // Fetch once when the component mounts
    fetchTodos();

    // Set up the interval to refresh data
    const reRunning = setInterval(fetchTodos, timeout * 1000);

    // Cleanup the interval when component unmounts OR timeout changes
    return () => clearInterval(reRunning);
  }, [timeout]); // 👈 dependencies array

  return { todos, loading };
}
Enter fullscreen mode Exit fullscreen mode

Why [timeout] as a Dependency?

The second argument of useEffect — the dependencies array — decides when React should re-run the effect.

  • If we leave it empty [], the effect runs only once on mount. That means if timeout changes later, the interval wouldn’t update.
  • By including [timeout], we’re telling React: 👉 “Whenever timeout changes, tear down the old interval and set up a new one.”

This ensures the auto-refresh logic always respects the latest interval value.


Using the Hook in a Component

function App() {
  const { todos, loading } = useTodos(6); // refresh every 6 seconds

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <>
      {todos.map((todo) => (
        <Track todo={todo} key={todo.id} />
      ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, App is clean:

  • No axios calls
  • No setInterval mess
  • Just pure rendering logic

Displaying Each Todo

function Track({ todo }) {
  return (
    <div className="font-black">
      {todo.id} - {todo.todo}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why Dependencies Matter So Much

Dependencies in useEffect are like a watch list. React monitors them, and if any value changes:

  1. React cleans up the old effect (important for intervals, subscriptions, listeners, etc.).
  2. React runs the effect again with the new values.

👉 Without [timeout], your interval would stay fixed to the old value, even if the parent passed a new one.

👉 With [timeout], the hook dynamically adapts, keeping your app flexible and bug-free.


Final Thoughts

This custom hook shows how to combine:

  • Data fetching
  • Auto-refresh with intervals
  • Proper cleanup
  • Dynamic dependency handling

The takeaway is:

  • Empty [] dependency → run once on mount.
  • Dynamic dependencies [timeout] → re-run when values change.

Top comments (0)