Improving Data Fetching in Next.js: Lessons from Moving Beyond useEffect

Published: (March 16, 2026 at 12:19 PM EDT)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

At Subito (Italy’s leading classifieds platform), we never paid much attention to how we made API calls in our web‑frontend microsites, all built on Next.js. We used a simple library based on Axios that handled requests, data modeling, and error management. On the server side it was a straightforward call; on the client side we relied on useEffect with state management for data and errors. It worked—until we started asking whether this was still the right approach.

The turning point was reading about the Cloudflare outage caused by excessive API calls triggered by useEffect. It made us reflect on our own stack:

  • Are we over‑fetching?
  • Are we accidentally triggering duplicate requests?
  • Could we overload our backend without realizing it?

Our primary goal became clear: embrace the standard and align with the official recommendations from React and Next.js.

Server‑Side Data Fetching

Since our architecture relies on the Next.js App Router to consume REST APIs, we needed a clean, well‑defined strategy that distinguishes between React Server Components and Client Components.

Native fetch with Next.js

Next.js extends the native fetch API, adding powerful caching and revalidation features out of the box. We replaced all Axios calls in our Server Components with this native fetch implementation.

Benefits:

  • Removed the ~13 KB Axios bundle from the client.
  • Eliminated multiple historical security advisories tied to Axios.
  • Gained built‑in caching and revalidation without extra configuration.

Client‑Side Data Fetching

On the client side we compared two popular libraries:

LibraryProsCons for our use case
TanStack Query (React Query)Massive ecosystem, advanced cache control, optimistic updatesRequires a QueryClient, a provider, and careful queryKey management—overkill for our simple needs.
SWR (Stale‑While‑Revalidate)Zero boilerplate, no mandatory providers, natural extension of the platform, client‑only by designFits perfectly with our already‑solved server‑side fetching.

Why SWR Won

  • Zero Boilerplate – just a hook: useSWR(key, fetcher).
  • Low Learning Curve – feels like a natural extension of Next.js.
  • Client‑Only Design – aligns with our separation of concerns.

Migrating from useEffect to SWR

Before (using useEffect)

import { useEffect, useState } from 'react';

const [items, setItems] = useState>([]);
const [isLoading, setIsLoading] = useState(true);

// load the Ads
useEffect(() => {
  getRecommendedItems(vertical)
    .then(setItems)
    .finally(() => setIsLoading(false));
}, [vertical]);

After (using SWR)

import useSWR from 'swr';

const { data: items = [], isLoading } = useSWR(
  SWR_KEYS.recommender(vertical),
  fetchRecommendedItems
);

Result: Removed two useState declarations and replaced a bulky useEffect block with a single, declarative hook.

Centralizing SWR Keys

export const SWR_KEYS = {
  recommender: (vertical: string) => ['recommender', 'items', vertical] as const,
  user: (userId: string) => ['user', userId] as const,
};
  • Type‑safe and predictable.
  • Avoids “magic strings” and ensures consistent caching.

Testing with SWR

Because SWR caches globally, we created a utility to isolate unit tests:

import { render } from '@testing-library/react';
import { SWRConfig } from 'swr';

export const renderWithSWR = (ui: React.ReactElement) => {
  return render(
    <SWRConfig value={{ provider: () => new Map() }}>
      {ui}
    </SWRConfig>
  );
};

// Usage example
// renderWithSWR(<MyComponent />);

Takeaways

  • Simplicity wins: For our scenario—few client‑side mutations, no shared cache between server and client, and reliance on backend caching—SWR + native fetch provided the cleanest solution.
  • Avoid over‑engineering: React Query is excellent for complex cache invalidation, optimistic updates, or tight server‑client state synchronization. It wasn’t needed for us.
  • Understand requirements first: Choose the tool that fits the problem, not the hype.

By moving away from manual useEffect fetching and adopting the SWR + Native Fetch combo, we achieved cleaner code and a more React‑standard way to handle data fetching. Sometimes the simplest tool is the most effective one.

0 views
Back to Blog

Related posts

Read more »