← Back to Blog

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 Watson15 min readtutorial
E

Emily Watson

Technical Writer & Web Developer

Emily 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 BasicsJavaScriptWeb APIsDeveloper ToolingTechnical Writing
15 min read

# 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
  • React Query for API Management
  • State Management Patterns
  • Form Handling with JSON
  • Local Storage & Persistence
  • TypeScript Integration
  • Performance Optimization
  • Error Handling
  • ---

    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 datasets

    Build type-safe, performant React applications with proper JSON handling!

    Share:

    Related Articles