JSON in React: State Management, API Integration & Best Practices 2026
Master JSON handling in React applications. Learn state management patterns, API integration with React Query, form handling, local storage, and performance optimization techniques.
Emily Watson
• Technical Writer & Web DeveloperEmily is a web developer and technical writer with 6 years of experience covering JavaScript ecosystems, developer tooling, and data formats. She specialises in making complex technical concepts approachable for developers at all levels, with a particular focus on JSON fundamentals, formatting best practices, and the tools developers reach for every day.
# JSON in React: State Management, API Integration & Best Practices 2026
React applications constantly work with JSON—fetching from APIs, managing state, persisting to local storage, and handling forms. This comprehensive guide covers modern patterns and best practices for JSON in React.
Table of Contents
---
Fetching JSON with Modern Hooks
Basic fetch with useState & useEffect
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(/api/users/${userId});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
return () => {
cancelled = true; // Prevent state updates after unmount
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Custom API Hook
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
setLoading(false);
}
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useApi<User[]>('/api/users');
if (loading) 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>
);
}
---
React Query for API Management
React Query eliminates boilerplate and provides caching, refetching, and more.
Installation
npm install @tanstack/react-query
Basic Setup
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 60 1000, // 5 minutes
cacheTime: 10 60 1000, // 10 minutes
refetchOnWindowFocus: false
}
}
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserProfile userId={1} />
</QueryClientProvider>
);
}
useQuery Hook
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(/api/users/${userId});
if (!response.ok) throw new Error('Failed to fetch user');
return response.json() as Promise<User>;
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
Mutations (POST, PUT, DELETE)
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser: Omit<User, 'id'>) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser)
});
return response.json() as Promise<User>;
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <div>Error: {mutation.error.message}</div>}
{mutation.isSuccess && <div>User created successfully!</div>}
</form>
);
}
Optimistic Updates
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (updatedUser) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['user', updatedUser.id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', updatedUser.id]);
// Optimistically update
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
return { previousUser };
},
onError: (err, updatedUser, context) => {
// Rollback on error
queryClient.setQueryData(
['user', updatedUser.id],
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
}
});
---
State Management Patterns
Context API for Global State
import { createContext, useContext, useState, ReactNode } from 'react';
interface AppState {
theme: 'light' | 'dark';
user: User | null;
}
interface AppContextValue {
state: AppState;
setState: React.Dispatch<React.SetStateAction<AppState>>;
}
const AppContext = createContext<AppContextValue | undefined>(undefined);
export function AppProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AppState>({
theme: 'light',
user: null
});
return (
<AppContext.Provider value={{ state, setState }}>
{children}
</AppContext.Provider>
);
}
export function useAppState() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context;
}
// Usage
function ThemeToggle() {
const { state, setState } = useAppState();
return (
<button
onClick={() => setState(prev => ({
...prev,
theme: prev.theme === 'light' ? 'dark' : 'light'
}))}
>
Current theme: {state.theme}
</button>
);
}
Zustand (Lightweight State Management)
npm install zustand
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
updateUser: (updates: Partial<User>) => void;
logout: () => void;
}
const useUserStore = create<UserStore>()(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
updateUser: (updates) =>
set((state) => ({
user: state.user ? { ...state.user, ...updates } : null
})),
logout: () => set({ user: null })
}),
{
name: 'user-storage' // localStorage key
}
)
);
// Usage
function UserProfile() {
const user = useUserStore((state) => state.user);
const logout = useUserStore((state) => state.logout);
if (!user) return <div>Not logged in</div>;
return (
<div>
<h1>{user.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
---
Form Handling with JSON
React Hook Form
npm install react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be at least 18 years old')
});
type UserFormData = z.infer<typeof userSchema>;
function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<UserFormData>({
resolver: zodResolver(userSchema)
});
const onSubmit = async (data: UserFormData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
console.log(result);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('name')} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input {...register('email')} placeholder="Email" type="email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input {...register('age', { valueAsNumber: true })} placeholder="Age" type="number" />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
---
Local Storage & Persistence
Custom Hook for Local Storage
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// Usage
function ThemeSelector() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
Session Storage
function useSessionStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
---
TypeScript Integration
Type-Safe API Client
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(${this.baseUrl}${endpoint});
if (!response.ok) throw new Error(HTTP error! status: ${response.status});
return response.json();
}
async post<T, B>(endpoint: string, body: B): Promise<T> {
const response = await fetch(${this.baseUrl}${endpoint}, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) throw new Error(HTTP error! status: ${response.status});
return response.json();
}
}
const api = new ApiClient('/api');
// Usage
const users = await api.get<User[]>('/users');
const newUser = await api.post<User, Omit<User, 'id'>>('/users', {
name: 'Alice',
email: 'alice@example.com'
});
---
Performance Optimization
Memoization
import { useMemo } from 'react';
function UserList({ users }: { users: User[] }) {
const sortedUsers = useMemo(() => {
console.log('Sorting users...');
return [...users].sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
return (
<ul>
{sortedUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Virtual Scrolling for Large Lists
npm install react-window
import { FixedSizeList } from 'react-window';
function LargeUserList({ users }: { users: User[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
{users[index].name}
</div>
);
return (
<FixedSizeList
height={400}
itemCount={users.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
}
---
Error Handling
Error Boundary
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<pre>{this.state.error?.message}</pre>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<UserProfile userId={1} />
</ErrorBoundary>
);
}
---
Conclusion
Modern React JSON handling patterns:
API Management: Use React Query for caching, refetching, and mutations State: Context API for global state, Zustand for lightweight stores Forms: React Hook Form + Zod for validation Persistence: Local/session storage hooks Performance: Memoization, virtual scrolling for large datasetsBuild type-safe, performant React applications with proper JSON handling!
Related Resources
Related Articles
JSON APIs and REST Services: Complete Development Guide
Learn to build and consume JSON-based REST APIs. Covers HTTP methods, authentication, best practices, and real-world implementation examples.
JavaScript JSON: Parse, Stringify, and Best Practices
Complete guide to JSON in JavaScript. Learn JSON.parse(), JSON.stringify(), error handling, and advanced techniques for web development.
Advanced TypeScript Patterns for JSON: Type Safety & Runtime Validation
Master advanced TypeScript techniques for JSON handling. Learn type guards, branded types, discriminated unions, Zod validation, and end-to-end type safety from API to UI.