Skip to content

useEffect vs React Query vs React Server Components: When to Use Each

One of the most confusing aspects of React development today is choosing the right approach for data fetching. Should I use useEffect? Should I reach for React Query? Or should I just use Server Components?

I see this question come up constantly in interviews and code reviews. The answer isn’t always straightforward, but there’s a clear decision framework that can help you choose the right tool every time.

The Short Answer

Here’s the quick decision guide:

  • Server Components: Use for SEO-critical pages, content sites, and initial page loads
  • React Query: Use for interactive client-side apps with complex state needs
  • useEffect: Avoid for data fetching entirely (I’ll explain why)

Let me walk you through each approach with real code examples.

Approach 1: useEffect (The Old Way)

This is how most of us learned to fetch data in React. It works, but it comes with significant drawbacks.

UserList.tsx
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Problems with useEffect for Data Fetching

  1. No caching: Every component mount triggers a new fetch
  2. Race conditions: Fast navigations can cause stale data to overwrite fresh data
  3. No deduplication: Multiple components requesting the same data will all fetch
  4. Manual everything: Loading states, error states, retries - you handle it all
  5. Strict mode issues: Development double-fetching can cause bugs

I only use useEffect for data fetching when I have no other choice. Even then, I immediately reach for a custom hook to abstract the complexity.

Approach 2: React Query (The Client-Side Powerhouse)

React Query (TanStack Query) solves almost all the problems with manual fetching. It provides caching, deduplication, background refetching, and optimistic updates out of the box.

UserListWithQuery.tsx
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
}
export function UserListWithQuery() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Why React Query Shines

The same component with React Query is simpler and more powerful:

  • Automatic caching: Data is cached and reused across components
  • Background refetching: Stale data is refreshed automatically
  • Deduplication: Multiple components share the same request
  • DevTools: Excellent debugging experience
  • Optimistic updates: Built-in support for mutations

When to Use React Query

  • Single-page applications with heavy interactivity
  • Dashboards and admin panels
  • Apps that need real-time updates
  • Complex state management involving server data
  • Client-side only applications

Approach 3: React Server Components (The Modern Default)

Server Components represent a paradigm shift. Instead of fetching data on the client, you fetch on the server and stream HTML to the browser.

UserListServer.tsx
// This runs on the server - no "use client" directive
interface User {
id: number;
name: string;
email: string;
}
async function getUsers(): Promise<User[]> {
const response = await fetch('https://api.example.com/users', {
// Enable caching with Next.js
next: { revalidate: 60 } // Revalidate every 60 seconds
});
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
}
export default async function UserListServer() {
const users = await getUsers();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Why Server Components Are Game-Changing

  • Zero JavaScript bundle impact: Data fetching logic stays on the server
  • Direct database access: No API layer needed
  • SEO-friendly: Content is in the initial HTML response
  • Faster perceived performance: Users see content immediately
  • Simpler mental model: Just async/await, no state management

When to Use Server Components

  • Blog posts and content pages
  • E-commerce product pages
  • Landing pages
  • Any page that needs SEO
  • Initial page load data

Decision Framework

Here’s a flowchart to help you decide:

Decision flowchart
+-------------------------+
| Need to fetch data? |
+-------------------------+
|
v
+-------------------------+
| Is SEO critical? |
+-------------------------+
/ \
YES NO
| |
v v
+----------------+ +---------------------+
| Server | | Client-side app? |
| Components | +---------------------+
+----------------+ / \
YES NO
| |
v v
+----------------+ +-----------------+
| React Query | | Server |
| | | Components |
+----------------+ +-----------------+

The Hybrid Approach

Real applications often need both. Here’s a pattern I use frequently:

HybridPattern.tsx
// Server Component - Initial data load
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { UsersList } from './UsersList';
async function getUsers() {
const response = await fetch('https://api.example.com/users');
return response.json();
}
export default async function UsersPage() {
const queryClient = new QueryClient();
// Prefetch on server
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: getUsers,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<h1>Users</h1>
<UsersList />
</HydrationBoundary>
);
}
UsersList.tsx
// Client Component - Interactive features
'use client';
import { useQuery } from '@tanstack/react-query';
export function UsersList() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
return (
<ul>
{users?.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => /* interactive feature */}>
Edit
</button>
</li>
))}
</ul>
);
}

This pattern gives you:

  1. Fast initial load with server-side rendering
  2. SEO benefits
  3. Client-side interactivity with React Query
  4. No flash of loading states

Common Interview Follow-up Questions

When discussing data fetching in interviews, expect these follow-ups:

Q: When would you NOT use Server Components? A: For highly interactive features like real-time chat, live dashboards, or when you need browser APIs like localStorage or window.

Q: How do you handle authentication with Server Components? A: Use middleware to check session tokens, then pass user context to server components. Avoid exposing sensitive data in client components.

Q: What about error boundaries? A: Server Components can use error.tsx files in Next.js. React Query has built-in error handling with retry logic.

Q: Can you mix all three approaches? A: Absolutely. Server Components for initial load, React Query for client-side updates, and avoid useEffect except for non-data-fetching side effects.

Key Takeaways

  1. Stop using useEffect for data fetching - It’s error-prone and manual
  2. Default to Server Components - They’re simpler and better for SEO
  3. Use React Query for client-side apps - When you need interactivity and caching
  4. Mix and match - Real apps benefit from hybrid approaches

The React ecosystem has evolved significantly. What worked in 2020 isn’t the best practice today. Choose your data fetching strategy based on your specific needs, not habit.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments