Learn/Language Integrations

JSON in React & Next.js — Data Fetching, State & Server Components

JSON is the lifeblood of React applications. Every API call, every piece of state, every config is JSON flowing through components. This guide covers every pattern: Server Component fetching, client-side state, SWR/TanStack Query caching, and the serialization traps that catch developers off guard.

How JSON Flows Through React

JSON Data Flow in Next.js App Router

1. Server Components — Async Data Fetching

In the Next.js App Router, Server Components are async by default. Fetch JSON directly — no hooks needed:

app/users/page.tsx — Server Componenttypescript
1interface User {
2 id: number;
3 name: string;
4 email: string;
5}
6
7export default async function UsersPage() {
8 const response = await fetch('https://api.example.com/users', {
9 next: { revalidate: 60 }, // Revalidate every 60 seconds
10 });
11 const users: User[] = await response.json();
12
13 return (
14 <ul>
15 {users.map(user => (
16 <li key={user.id}>{user.name} — {user.email}</li>
17 ))}
18 </ul>
19 );
20}

Tip

Next.js automatically deduplicates identical fetch() calls across components in the same render. If three components fetch /api/user, only one HTTP request is made.

Caching Strategies

Strategyfetch OptionWhen to Use
Static (default)cache: "force-cache"Data that rarely changes (blog posts, docs)
Revalidatenext: { revalidate: 60 }Data that changes periodically (products, feeds)
Dynamiccache: "no-store"Data that changes per request (user sessions, live data)

2. Client Components — State & Effects

Manual Fetch with useState

Client-side JSON fetchtypescript
1'use client';
2import { useEffect, useState } from 'react';
3
4interface Post {
5 id: number;
6 title: string;
7 body: string;
8}
9
10export function PostList() {
11 const [posts, setPosts] = useState<Post[]>([]);
12 const [loading, setLoading] = useState(true);
13 const [error, setError] = useState<string | null>(null);
14
15 useEffect(() => {
16 const controller = new AbortController();
17
18 fetch('/api/posts', { signal: controller.signal })
19 .then(res => {
20 if (!res.ok) throw new Error(`HTTP ${res.status}`);
21 return res.json();
22 })
23 .then((data: Post[]) => setPosts(data))
24 .catch(err => {
25 if (err.name !== 'AbortError') setError(err.message);
26 })
27 .finally(() => setLoading(false));
28
29 return () => controller.abort();
30 }, []);
31
32 if (loading) return <div>Loading...</div>;
33 if (error) return <div>Error: {error}</div>;
34 return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
35}

SWR — Stale-While-Revalidate

SWR for JSON APIstypescript
1'use client';
2import useSWR from 'swr';
3
4const fetcher = (url: string) => fetch(url).then(res => res.json());
5
6interface User { id: number; name: string; email: string }
7
8export function UserProfile({ userId }: { userId: string }) {
9 const { data, error, isLoading, mutate } = useSWR<User>(
10 `/api/users/${userId}`,
11 fetcher,
12 {
13 revalidateOnFocus: true,
14 dedupingInterval: 5000,
15 }
16 );
17
18 if (isLoading) return <div>Loading...</div>;
19 if (error) return <div>Failed to load</div>;
20 if (!data) return null;
21
22 return (
23 <div>
24 <h2>{data.name}</h2>
25 <p>{data.email}</p>
26 <button onClick={() => mutate()}>Refresh</button>
27 </div>
28 );
29}

TanStack Query — Full CRUD

TanStack Query for mutationstypescript
1'use client';
2import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
3
4interface Todo { id: number; title: string; completed: boolean }
5
6export function TodoApp() {
7 const queryClient = useQueryClient();
8
9 const { data: todos = [], isLoading } = useQuery<Todo[]>({
10 queryKey: ['todos'],
11 queryFn: () => fetch('/api/todos').then(res => res.json()),
12 });
13
14 const addTodo = useMutation({
15 mutationFn: (title: string) =>
16 fetch('/api/todos', {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/json' },
19 body: JSON.stringify({ title, completed: false }),
20 }).then(res => res.json()),
21 onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
22 });
23
24 return (
25 <div>
26 <button onClick={() => addTodo.mutate('New task')}>Add Todo</button>
27 {isLoading ? <p>Loading...</p> : (
28 <ul>{todos.map(t => <li key={t.id}>{t.title}</li>)}</ul>
29 )}
30 </div>
31 );
32}
FeatureManual fetchSWRTanStack Query
CachingManual (useState)Automatic (stale-while-revalidate)Automatic + granular
MutationsManualManual mutate()Built-in useMutation
DeduplicationNoneBuilt-inBuilt-in
DevtoolsNoneSWR DevtoolsReact Query Devtools
Bundle size0 KB~4 KB~12 KB
Best forSimple one-off fetchesRead-heavy appsFull CRUD apps

3. Serialization Boundary Traps

When passing data from Server to Client Components, React serializes props. Several JavaScript types do not survive this boundary:

TypeSurvives Serialization?Workaround
string, number, boolean, nullYesNo action needed
Plain objects and arraysYesNo action needed
DateNo — becomes stringPass ISO string, parse on client
undefinedNo — stripped from objectsUse null instead
Map / SetNoConvert to array/object
BigIntNoConvert to string
FunctionsNoCannot pass across boundary
class instancesNoConvert to plain object
Handling Date serializationtypescript
1// Server Component
2async function EventPage() {
3 const event = await getEvent(1);
4 return (
5 <EventCard
6 title={event.title}
7 // Pass Date as ISO string
8 startAt={event.startAt.toISOString()}
9 />
10 );
11}
12
13// Client Component
14'use client';
15function EventCard({ title, startAt }: { title: string; startAt: string }) {
16 const date = new Date(startAt);
17 return (
18 <div>
19 <h2>{title}</h2>
20 <time dateTime={startAt}>{date.toLocaleDateString()}</time>
21 </div>
22 );
23}

4. Server Actions — Mutating JSON

Server Action returning JSONtypescript
1// app/actions.ts
2'use server';
3
4interface CreateUserResult {
5 success: boolean;
6 user?: { id: number; name: string };
7 error?: string;
8}
9
10export async function createUser(formData: FormData): Promise<CreateUserResult> {
11 const name = formData.get('name') as string;
12 if (!name || name.length < 2) {
13 return { success: false, error: 'Name must be at least 2 characters' };
14 }
15
16 const response = await fetch('https://api.example.com/users', {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/json' },
19 body: JSON.stringify({ name }),
20 });
21
22 if (!response.ok) {
23 return { success: false, error: 'API request failed' };
24 }
25
26 const user = await response.json();
27 return { success: true, user };
28}

5. JSON in React State Patterns

Immutable Updates

1const [user, setUser] = useState({ name: 'Alice', settings: { theme: 'dark' } });
2
3// WRONG — mutates state directly
4user.settings.theme = 'light';
5setUser(user);
6
7// CORRECT — immutable spread
8setUser(prev => ({
9 ...prev,
10 settings: { ...prev.settings, theme: 'light' },
11}));

Zustand for Complex JSON State

1import { create } from 'zustand';
2
3interface AppState {
4 config: Record<string, unknown>;
5 updateConfig: (path: string, value: unknown) => void;
6}
7
8const useStore = create<AppState>((set) => ({
9 config: { theme: 'dark', language: 'en', notifications: true },
10 updateConfig: (path, value) =>
11 set(state => ({
12 config: { ...state.config, [path]: value },
13 })),
14}));

Try It — Validate an API Response

Try It Yourself

A typical API response shape consumed by React components

Frequently Asked Questions

How do I fetch JSON in a React Server Component?
Use the native fetch() directly inside the async component body — no useEffect or useState needed. Next.js extends fetch with caching and revalidation options. The component renders on the server, so the JSON is fetched before HTML reaches the browser.
Can I use JSON.parse in React Server Components?
Yes, but you rarely need to. fetch().json() already returns a parsed object. If you are reading raw JSON from a file or database, JSON.parse works normally in Server Components since they run on the server with full Node.js access.
Why does my Date become a string when passed from Server to Client Component?
React serializes Server Component props using a superset of JSON. Date, Map, Set, BigInt, undefined, and functions cannot survive the serialization boundary. Convert dates to ISO strings on the server and parse them on the client.
Should I use SWR or TanStack Query for JSON APIs?
Both are excellent. SWR is simpler and lighter (built by the Next.js team). TanStack Query is more powerful with mutations, infinite queries, and devtools. For read-heavy apps, SWR is usually enough. For CRUD-heavy apps, TanStack Query is better.
How do I handle loading and error states for JSON fetches in React?
In Server Components, use loading.tsx and error.tsx boundary files (Next.js App Router). In Client Components, use SWR or TanStack Query which provide isLoading, error, and data states out of the box. For manual fetch, use useState + useEffect with try/catch.