Claude
Skills
Sign in
Back

react-data-fetching

Included with Lifetime
$97 forever

Teaches modern React data fetching patterns with TanStack Query, SWR, and Suspense. Use when implementing caching, deduplication, optimistic updates, or parallel loading in React applications.

Web Dev

What this skill does


# React Data Fetching Patterns

## Table of Contents

- [When to Use](#when-to-use)
- [Instructions](#instructions)
- [Details](#details)
- [Source](#source)

Production-ready patterns for fetching, caching, and synchronizing server data in React applications. These patterns are framework-agnostic — they work whether you're using Vite + React Router, Next.js, Remix, or a custom setup.

## When to Use

Reference these patterns when:
- Adding data fetching to components
- Replacing `useEffect` + `fetch` with a proper data layer
- Implementing caching, deduplication, or optimistic updates
- Debugging waterfall loading patterns
- Choosing between data fetching libraries

## Instructions

- Apply these patterns during code generation, review, and refactoring. When you see fetch-in-effect without caching or deduplication, suggest the appropriate pattern.

## Details

### Overview

The most common performance problem in React apps is **request waterfalls** — sequential fetches that could run in parallel. The second most common problem is **redundant fetches** — multiple components fetching the same data independently. The patterns below address both, starting with the highest-impact fixes.

---

### 1. Parallelize Independent Fetches with `Promise.all`

**Impact: CRITICAL** — Eliminates sequential waterfalls for 2-10x improvement.

When multiple fetches have no dependencies on each other, run them concurrently.

**Avoid — sequential (3 round trips):**

```typescript
async function loadDashboard() {
  const user = await fetchUser()
  const posts = await fetchPosts()
  const notifications = await fetchNotifications()
  return { user, posts, notifications }
}
```

**Prefer — parallel (1 round trip):**

```typescript
async function loadDashboard() {
  const [user, posts, notifications] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchNotifications(),
  ])
  return { user, posts, notifications }
}
```

When fetches have partial dependencies (B depends on A, but C doesn't), start independent work immediately:

```typescript
async function loadPage() {
  const userPromise = fetchUser()
  const configPromise = fetchConfig()

  const user = await userPromise
  const [config, posts] = await Promise.all([
    configPromise,
    fetchPosts(user.id), // depends on user
  ])
  return { user, config, posts }
}
```

---

### 2. Defer Await Until the Value Is Needed

**Impact: HIGH** — Starts work earlier without blocking on results you don't need yet.

A common mistake is to `await` each promise immediately, even when subsequent code doesn't need the result right away. Start the promise early, then `await` it at the point where you actually read the value.

**Avoid — blocks unnecessarily:**

```typescript
async function loadProfile(userId: string) {
  const user = await fetchUser(userId)       // waits here
  const prefs = await fetchPreferences()     // starts only after user resolves
  const avatar = buildAvatarUrl(user.avatar)
  return { user, prefs, avatar }
}
```

**Prefer — start early, await late:**

```typescript
async function loadProfile(userId: string) {
  const userPromise = fetchUser(userId)      // starts immediately
  const prefsPromise = fetchPreferences()    // starts immediately

  const user = await userPromise             // await when needed
  const avatar = buildAvatarUrl(user.avatar)
  const prefs = await prefsPromise           // may already be resolved

  return { user, prefs, avatar }
}
```

This is complementary to `Promise.all` — use defer-await when you need intermediate results between fetches, and `Promise.all` when you can wait for everything at once.

---

### 3. Use TanStack Query for Client-Side Data

**Impact: CRITICAL** — Automatic caching, deduplication, revalidation, and error handling.

Raw `useEffect` + `fetch` lacks caching, deduplication, retry, and background refresh. Use a data fetching library.

**Avoid — no caching, no dedup, no error handling:**

```tsx
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setLoading(true)
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser)
      .finally(() => setLoading(false))
  }, [userId])

  if (loading) return <Skeleton />
  return <div>{user?.name}</div>
}
```

**Prefer — TanStack Query (recommended for Vite + React apps):**

```tsx
import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  })

  if (isLoading) return <Skeleton />
  return <div>{user?.name}</div>
}
```

TanStack Query is the strongest choice for Vite apps — it's framework-agnostic, has built-in `useSuspenseQuery`, devtools, infinite queries, optimistic mutations, and offline support. SWR is a lighter alternative that covers the basics (dedup, caching, revalidation) but has fewer features for complex mutation workflows.

Both give you: request deduplication, stale-while-revalidate caching, automatic retries, and background refresh.

**Setup for Vite apps:**

```tsx
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000, // 1 minute
      retry: 2,
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)
```

---

### 4. Use Suspense for Declarative Loading States

**Impact: HIGH** — Cleaner code, automatic loading coordination, streaming support.

Suspense lets you declare loading boundaries in the component tree instead of managing `isLoading` state in every component.

**Avoid — manual loading orchestration:**

```tsx
function Dashboard() {
  const { data: user, isLoading: userLoading } = useQuery(userQuery)
  const { data: stats, isLoading: statsLoading } = useQuery(statsQuery)

  if (userLoading || statsLoading) return <FullPageSpinner />
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
    </div>
  )
}
```

**Prefer — Suspense boundaries:**

```tsx
function Dashboard() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <DashboardContent />
    </Suspense>
  )
}

function DashboardContent() {
  const { data: user } = useSuspenseQuery(userQuery)
  const { data: stats } = useSuspenseQuery(statsQuery)
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
    </div>
  )
}
```

For independent sections, use separate Suspense boundaries so they load independently:

```tsx
function Dashboard() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <UserHeader />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel />
      </Suspense>
    </div>
  )
}
```

TanStack Query provides `useSuspenseQuery` and SWR provides `{ suspense: true }` option.

---

### 5. Prefetch Data Before Navigation

**Impact: HIGH** — Eliminates loading states on page transitions.

Start fetching data before the user commits to a navigation — on hover, focus, or route preload.

**With TanStack Query:**

```tsx
import { useQueryClient } from '@tanstack/react-query'

function ProjectLink({ projectId }: { projectId: string }) {
  const queryClient = useQueryClient()

  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['project', projectId],
      queryFn: () => fetchProject(projectId),
      staleTime: 30_000,
    })
  }

  return (
    <Link
      to={`/projects/${projectId}`}
      onMouseEnter={prefetch}
      onFocus={prefetch}
    >
      View Project
    </Link>
  )
}
```

**With React Router loaders (Vite apps):**

```tsx
// routes.tsx
const routes = [
  {
    path: '/projects/:id',
    loader: 
Files: 1
Size: 14.7 KB
Complexity: 23/100
Category: Web Dev

Related in Web Dev