Skip to content
Back to skills

3054 React Data Fetching 63ae55d9

ASecurity

Fetching data from APIs

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmenttypescriptgoreactnextjsnodeapidatabasefrontendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 3054-react-data-fetching_63ae55d9 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 3054 React Data Fetching 63ae55d9?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 3054 React Data Fetching 63ae55d9
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-3054-react-data-fetching-63ae55d9/badge)](https://www.skillsdirectory.com/skills/tools-only-3054-react-data-fetching-63ae55d9)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: frontend-react-data-fetching
description: Fetching data from APIs
---


# React Data Fetching

**Scope**: SWR, React Query, Server Actions, caching strategies, optimistic updates
**Lines**: ~310
**Last Updated**: 2025-10-18

## When to Use This Skill

Activate this skill when:
- Fetching data from APIs
- Implementing caching strategies
- Handling loading and error states
- Optimistic UI updates
- Infinite scrolling/pagination
- Real-time data synchronization
- Choosing between SWR and React Query

## Core Concepts

### Client vs Server Data

**Client State** - UI state, form inputs, modals
```tsx
const [isOpen, setIsOpen] = useState(false);
```

**Server State** - Data from API, database
```tsx
const { data: posts } = useSWR('/api/posts');
```

**Key differences**:
- Server state is async, remote, shared
- Server state can be stale
- Server state needs caching, revalidation
- Server state is owned by server

---

## SWR (Stale-While-Revalidate)

### Basic Usage

```tsx
import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(res => res.json());

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load</div>;

  return <div>Hello {data.name}!</div>;
}
```

### Global Configuration

```tsx
// app/layout.tsx or _app.tsx
import { SWRConfig } from 'swr';

const fetcher = (url: string) => fetch(url).then(res => {
  if (!res.ok) throw new Error('API error');
  return res.json();
});

export default function App({ children }) {
  return (
    <SWRConfig value={{
      fetcher,
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
      dedupingInterval: 2000,
    }}>
      {children}
    </SWRConfig>
  );
}
```

### Mutations

```tsx
function UpdateProfile() {
  const { data: user, mutate } = useSWR('/api/user', fetcher);

  const updateName = async (newName: string) => {
    // Optimistic update
    mutate({ ...user, name: newName }, false);

    // API call
    await fetch('/api/user', {
      method: 'PATCH',
      body: JSON.stringify({ name: newName }),
    });

    // Revalidate
    mutate();
  };

  return (
    <input
      value={user?.name}
      onChange={(e) => updateName(e.target.value)}
    />
  );
}
```

### Dependent Queries

```tsx
function UserPosts({ userId }: { userId: string }) {
  // First fetch user
  const { data: user } = useSWR(`/api/users/${userId}`, fetcher);

  // Then fetch user's posts (only when user is loaded)
  const { data: posts } = useSWR(
    user ? `/api/users/${user.id}/posts` : null,
    fetcher
  );

  if (!user) return <div>Loading user...</div>;
  if (!posts) return <div>Loading posts...</div>;

  return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
```

### Pagination

```tsx
function PostList() {
  const [page, setPage] = useState(1);

  const { data, isLoading } = useSWR(
    `/api/posts?page=${page}&limit=10`,
    fetcher
  );

  return (
    <div>
      {data?.posts.map(post => <div key={post.id}>{post.title}</div>)}

      <button onClick={() => setPage(page - 1)} disabled={page === 1}>
        Previous
      </button>
      <button onClick={() => setPage(page + 1)}>
        Next
      </button>
    </div>
  );
}
```

### Infinite Scroll

```tsx
import useSWRInfinite from 'swr/infinite';

function InfinitePostList() {
  const getKey = (pageIndex: number, previousPageData: any) => {
    // Reached the end
    if (previousPageData && !previousPageData.posts.length) return null;

    // First page
    return `/api/posts?page=${pageIndex + 1}&limit=10`;
  };

  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);

  const posts = data ? data.flatMap(page => page.posts) : [];
  const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
  const isEmpty = data?.[0]?.posts.length === 0;
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.posts.length < 10);

  return (
    <div>
      {posts.map(post => <div key={post.id}>{post.title}</div>)}

      {!isReachingEnd && (
        <button onClick={() => setSize(size + 1)} disabled={isLoadingMore}>
          {isLoadingMore ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
```

---

## React Query (TanStack Query)

### Basic Usage

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

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await fetch('/api/posts');
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {data.map((post: Post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}
```

### Query Client Setup

```tsx
// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1 minute
        refetchOnWindowFocus: false,
      },
    },
  }));

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
```

### Mutations

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

function CreatePost() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newPost: { title: string; content: string }) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      if (!res.ok) throw new Error('Failed to create');
      return res.json();
    },
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Post', content: 'Content' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create Post'}
    </button>
  );
}
```

### Optimistic Updates

```tsx
function UpdatePost({ post }: { post: Post }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (updatedPost: Post) => {
      const res = await fetch(`/api/posts/${post.id}`, {
        method: 'PUT',
        body: JSON.stringify(updatedPost),
      });
      return res.json();
    },

    // Optimistic update
    onMutate: async (updatedPost) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['posts', post.id] });

      // Snapshot previous value
      const previousPost = queryClient.getQueryData(['posts', post.id]);

      // Optimistically update cache
      queryClient.setQueryData(['posts', post.id], updatedPost);

      // Return context with snapshot
      return { previousPost };
    },

    // Rollback on error
    onError: (err, updatedPost, context) => {
      queryClient.setQueryData(['posts', post.id], context?.previousPost);
    },

    // Always refetch after error or success
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['posts', post.id] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ ...post, title: 'Updated' })}>
      Update
    </button>
  );
}
```

### Pagination

```tsx
function PaginatedPosts() {
  const [page, setPage] = useState(1);

  const { data, isLoading, isPlaceholderData } = useQuery({
    queryKey: ['posts', page],
    queryFn: async () => {
      const res = await fetch(`/api/posts?page=${page}&limit=10`);
      return res.json();
    },
    placeholderData: (previousData) => previousData, // Keep previous data while fetching
  });

  return (
    <div>
      {data?.posts.map((post: Post) => (
        <div key={post.id}>{post.title}</div>
      ))}

      <button
        onClick={() => setPage(page - 1)}
        disabled={page === 1}
      >
        Previous
      </button>
      <button
        onClick={() => setPage(page + 1)}
        disabled={isPlaceholderData || !data?.hasMore}
      >
        Next
      </button>
    </div>
  );
}
```

### Infinite Queries

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

function InfinitePosts() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 1 }) => {
      const res = await fetch(`/api/posts?page=${pageParam}&limit=10`);
      return res.json();
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    },
    initialPageParam: 1,
  });

  const posts = data?.pages.flatMap(page => page.posts) ?? [];

  return (
    <div>
      {posts.map((post: Post) => (
        <div key={post.id}>{post.title}</div>
      ))}

      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
```

---

## Next.js Server Actions

### Basic Server Action

```tsx
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await prisma.post.create({
    data: { title, content }
  });

  revalidatePath('/posts');
}

// app/posts/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  );
}
```

### Client-Side with useFormStatus

```tsx
// app/components/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  );
}

// app/posts/new/page.tsx
import { createPost } from '@/app/actions';
import { SubmitButton } from '@/app/components/SubmitButton';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  );
}
```

### useFormState for Validation

```tsx
// app/actions.ts
'use server';

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validation
  if (!title || title.length < 3) {
    return { error: 'Title must be at least 3 characters' };
  }

  await prisma.post.create({ data: { title, content } });

  return { success: true };
}

// app/components/PostForm.tsx
'use client';

import { useFormState } from 'react-dom';
import { createPost } from '@/app/actions';

export function PostForm() {
  const [state, formAction] = useFormState(createPost, null);

  return (
    <form action={formAction}>
      <input name="title" required />
      {state?.error && <p className="error">{state.error}</p>}
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  );
}
```

---

## Caching Strategies

### Stale-While-Revalidate (SWR Pattern)

```tsx
// Return stale data immediately, revalidate in background
const { data } = useSWR('/api/posts', fetcher, {
  revalidateOnFocus: true,    // Revalidate when window regains focus
  revalidateOnReconnect: true, // Revalidate when network reconnects
  dedupingInterval: 2000,      // Dedupe requests within 2 seconds
});
```

### Cache and Network (React Query)

```tsx
const { data } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 5 * 60 * 1000,    // 5 minutes before stale
  cacheTime: 10 * 60 * 1000,   // 10 minutes in cache
  refetchOnWindowFocus: true,
  refetchOnReconnect: true,
});
```

### Time-Based Revalidation (Next.js)

```tsx
// app/posts/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
```

### On-Demand Revalidation (Next.js)

```tsx
// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function createPost(data: any) {
  await prisma.post.create({ data });

  // Revalidate specific paths
  revalidatePath('/posts');
  revalidatePath('/');

  // Or revalidate by tag
  revalidateTag('posts');
}

// Fetch with tag
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }
  });
  return res.json();
}
```

---

## SWR vs React Query

### Comparison

| Feature | SWR | React Query |
|---------|-----|-------------|
| **Bundle Size** | 4.3kb | 12.9kb |
| **Complexity** | Simple | More features |
| **DevTools** | No | Yes |
| **Mutations** | Manual | Built-in |
| **TypeScript** | Good | Excellent |
| **Best For** | Simple apps, Next.js | Complex apps, large teams |

### When to Use SWR

```
✅ Next.js projects (official Vercel library)
✅ Simple data fetching needs
✅ Small bundle size priority
✅ Straightforward caching
✅ Quick setup
```

### When to Use React Query

```
✅ Complex mutations
✅ Optimistic updates
✅ DevTools needed
✅ Pagination/infinite scroll
✅ Large apps with many queries
✅ Advanced caching strategies
```

---

## Quick Reference

### SWR Basic Pattern

```tsx
const { data, error, isLoading, mutate } = useSWR(key, fetcher, options);

// Mutate
mutate(newData, false); // Optimistic
mutate();               // Revalidate
```

### React Query Basic Pattern

```tsx
const { data, error, isLoading } = useQuery({
  queryKey: ['key'],
  queryFn: fetchFn,
});

const mutation = useMutation({
  mutationFn: createFn,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['key'] }),
});
```

### Server Actions Pattern

```tsx
'use server';
export async function action(formData: FormData) {
  // Mutation
  revalidatePath('/path');
}

// Client
<form action={action}>...</form>
```

---

## Common Anti-Patterns

❌ **Fetching in useEffect**: Use SWR/React Query instead
✅ Dedicated data fetching library

❌ **Global state for server data**: Server state !== client state
✅ Keep server data in SWR/React Query cache

❌ **No loading/error states**: Poor UX
✅ Always handle loading, error, empty states

❌ **Not caching**: Unnecessary refetches
✅ Use staleTime, cacheTime appropriately

---

## Related Skills

- `react-state-management.md` - Client state (Zustand, Context)
- `nextjs-app-router.md` - Server Components, Server Actions
- `frontend-performance.md` - Request deduplication, prefetching
- `react-form-handling.md` - Form validation, submission

---

**Last Updated**: 2025-10-18
**Format Version**: 1.0 (Atomic)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.