When working with APIs in React, a common pattern is to use useEffect
and useState
with a library like Axios to fetch data. While this works, it can become verbose, especially when you add polling, caching, or revalidation. This is where SWR comes in.
In this article, we’ll walk through:
- A manual approach with
useEffect
. - A buggy hybrid where SWR and the manual state approach are mixed.
- A clean SWR solution that replaces the boilerplate.
1. Manual Hook With useEffect
Here’s a simple custom hook that fetches todos every timeout
seconds:
import { useEffect, useState } from "react";
import axios from "axios";
function useTodos(timeout) {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchTodos = () => {
axios.get("https://dummyjson.com/todos").then((res) => {
setTodos(res.data.todos);
setLoading(false);
});
};
// Initial fetch
fetchTodos();
// Poll every `timeout` seconds
const reRunning = setInterval(fetchTodos, timeout * 1000);
return () => clearInterval(reRunning);
}, [timeout]);
return { todos, loading };
}
And the component using it:
function App() {
const { todos, loading } = useTodos(6);
if (loading) return <p>Loading...</p>;
return (
<>
{todos.map((todo) => (
<Track todo={todo} key={todo.id} />
))}
</>
);
}
function Track({ todo }) {
return (
<div className="font-black">
{todo.id} - {todo.todo}
</div>
);
}
This works fine, but you have to manage state, effects, and cleanup yourself.
2. Mixing useEffect
and SWR (What Went Wrong)
When switching to SWR, you might be tempted to leave parts of the old code around. For example:
const { data, error, isLoading } = useSWR("https://dummyjson.com/todos", fetcher);
if (loading) return <p>Loading...</p>; // ❌ wrong: "loading" doesn’t exist
{todos.map((todo) => ...)} // ❌ wrong: "todos" comes from old hook
Issues here:
- The fetcher function was incorrect (
.catch
dangling). - You used
loading
andtodos
, which belong to the old custom hook, not SWR. -
todo.key
was used instead oftodo.id
.
This kind of hybrid setup leads to broken logic.
3. Clean SWR Approach
With SWR, you can remove useEffect
, useState
, and manual intervals entirely.
import useSWR from "swr";
import axios from "axios";
const fetcher = (url) => axios.get(url).then((res) => res.data);
function App() {
const { data, error, isLoading } = useSWR(
"https://dummyjson.com/todos",
fetcher,
{ refreshInterval: 6000 } // re-fetch every 6 seconds
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Failed to load todos</p>;
return (
<>
{data.todos.map((todo) => (
<Track todo={todo} key={todo.id} />
))}
</>
);
}
function Track({ todo }) {
return (
<div className="font-black">
{todo.id} - {todo.todo}
</div>
);
}
export default App;
Why This Is Better
- Less boilerplate: no need for manual state or intervals.
- Built-in caching: data stays fresh even when navigating between pages.
- Automatic revalidation: background fetching keeps your UI up-to-date.
-
Simple polling: just add
refreshInterval
.
4. Bonus: Custom Hook With SWR
If you still like the useTodos(timeout)
style API, you can wrap SWR like this:
function useTodos(timeout) {
const { data, error, isLoading } = useSWR(
"https://dummyjson.com/todos",
fetcher,
{ refreshInterval: timeout * 1000 }
);
return {
todos: data?.todos || [],
error,
loading: isLoading,
};
}
Then use it exactly like before:
const { todos, loading, error } = useTodos(6);
✅ Takeaway: SWR eliminates a lot of repetitive state and effect logic, giving you a cleaner, more declarative way to fetch and manage remote data in React.
Top comments (0)