How JSON Flows Through React
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}67export default async function UsersPage() {8 const response = await fetch('https://api.example.com/users', {9 next: { revalidate: 60 }, // Revalidate every 60 seconds10 });11 const users: User[] = await response.json();1213 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
| Strategy | fetch Option | When to Use |
|---|---|---|
| Static (default) | cache: "force-cache" | Data that rarely changes (blog posts, docs) |
| Revalidate | next: { revalidate: 60 } | Data that changes periodically (products, feeds) |
| Dynamic | cache: "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';34interface Post {5 id: number;6 title: string;7 body: string;8}910export function PostList() {11 const [posts, setPosts] = useState<Post[]>([]);12 const [loading, setLoading] = useState(true);13 const [error, setError] = useState<string | null>(null);1415 useEffect(() => {16 const controller = new AbortController();1718 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));2829 return () => controller.abort();30 }, []);3132 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';34const fetcher = (url: string) => fetch(url).then(res => res.json());56interface User { id: number; name: string; email: string }78export 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 );1718 if (isLoading) return <div>Loading...</div>;19 if (error) return <div>Failed to load</div>;20 if (!data) return null;2122 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';34interface Todo { id: number; title: string; completed: boolean }56export function TodoApp() {7 const queryClient = useQueryClient();89 const { data: todos = [], isLoading } = useQuery<Todo[]>({10 queryKey: ['todos'],11 queryFn: () => fetch('/api/todos').then(res => res.json()),12 });1314 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 });2324 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}| Feature | Manual fetch | SWR | TanStack Query |
|---|---|---|---|
| Caching | Manual (useState) | Automatic (stale-while-revalidate) | Automatic + granular |
| Mutations | Manual | Manual mutate() | Built-in useMutation |
| Deduplication | None | Built-in | Built-in |
| Devtools | None | SWR Devtools | React Query Devtools |
| Bundle size | 0 KB | ~4 KB | ~12 KB |
| Best for | Simple one-off fetches | Read-heavy apps | Full 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:
| Type | Survives Serialization? | Workaround |
|---|---|---|
| string, number, boolean, null | Yes | No action needed |
| Plain objects and arrays | Yes | No action needed |
| Date | No — becomes string | Pass ISO string, parse on client |
| undefined | No — stripped from objects | Use null instead |
| Map / Set | No | Convert to array/object |
| BigInt | No | Convert to string |
| Functions | No | Cannot pass across boundary |
| class instances | No | Convert to plain object |
Handling Date serializationtypescript
1// Server Component2async function EventPage() {3 const event = await getEvent(1);4 return (5 <EventCard6 title={event.title}7 // Pass Date as ISO string8 startAt={event.startAt.toISOString()}9 />10 );11}1213// Client Component14'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.ts2'use server';34interface CreateUserResult {5 success: boolean;6 user?: { id: number; name: string };7 error?: string;8}910export 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 }1516 const response = await fetch('https://api.example.com/users', {17 method: 'POST',18 headers: { 'Content-Type': 'application/json' },19 body: JSON.stringify({ name }),20 });2122 if (!response.ok) {23 return { success: false, error: 'API request failed' };24 }2526 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' } });23// WRONG — mutates state directly4user.settings.theme = 'light';5setUser(user);67// CORRECT — immutable spread8setUser(prev => ({9 ...prev,10 settings: { ...prev.settings, theme: 'light' },11}));Zustand for Complex JSON State
1import { create } from 'zustand';23interface AppState {4 config: Record<string, unknown>;5 updateConfig: (path: string, value: unknown) => void;6}78const 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
Try These Tools
Continue Learning
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.