DEV Community

Cover image for Deeper Dive into State Management in React & React Native with TypeScript (2025 Edition)
Serif COLAKEL
Serif COLAKEL

Posted on

Deeper Dive into State Management in React & React Native with TypeScript (2025 Edition)

Introduction: The "State" of the Union in 2025

State management has evolved from a technical consideration to a strategic architectural decision that can make or break your application. In 2025, with applications handling real-time data, AI integrations, and complex user workflows, choosing the right state management approach is more critical than ever. The landscape has matured, with solutions like Redux Toolkit becoming the enterprise standard, while nimble alternatives like Zustand and Jotai dominate the mid-market. This comprehensive guide explores the modern state management ecosystem with practical, real-world TypeScript examples that you can apply directly to your projects.

Why State Management Is More Important Than Ever in 2025

Consider a food delivery application operating in a metropolitan area during peak hours. The app must handle: real-time order tracking, driver location updates, inventory changes, promotional banners, user authentication, and payment processing—all simultaneously. Using only local state and Context API would result in:

  1. Performance Catastrophe: A single driver location update would trigger re-renders across the entire application, causing noticeable lag for users trying to browse menus or place orders.

  2. State Synchronization Nightmares: Different components showing conflicting information about order status or inventory availability due to fragmented state management.

  3. Debugging Hell: Tracing why a promotional banner disappeared or an order failed to process becomes nearly impossible without centralized state and devtools.

  4. Team Collaboration Challenges: Multiple developers working on different features would constantly conflict with state structure changes without a predictable state container.

  5. Offline Functionality Limitations: Implementing robust offline capabilities and data synchronization requires sophisticated state management patterns that basic solutions cannot provide.

The 2025 application landscape demands state management solutions that can handle:

  • Real-time data synchronization across multiple devices
  • AI/ML model state management
  • Cross-platform consistency (Web, iOS, Android)
  • Offline-first capabilities with conflict resolution
  • Micro-frontend architecture support

The Big Players: Real-World Implementation Deep Dive

1. Context API + Hooks: The Lightweight Champion

Real-World Scenario: A multi-language news application with theme support (light/dark/reading mode) and user preference persistence.

Why it works here: Theme and language preferences change infrequently and are consumed by virtually every component. The low update frequency minimizes performance concerns.

TypeScript Implementation:

// types/theme.ts
export type ThemeMode = "light" | "dark" | "reading";
export type Language = "en" | "es" | "fr" | "de";

export interface AppSettings {
  theme: ThemeMode;
  language: Language;
  fontSize: number;
  reduceAnimations: boolean;
}

// context/AppSettingsContext.tsx
import React, { createContext, useContext, useReducer, ReactNode } from "react";
import { AppSettings, ThemeMode, Language } from "../types/theme";

type Action =
  | { type: "SET_THEME"; payload: ThemeMode }
  | { type: "SET_LANGUAGE"; payload: Language }
  | { type: "SET_FONT_SIZE"; payload: number }
  | { type: "TOGGLE_ANIMATIONS" };

const settingsReducer = (state: AppSettings, action: Action): AppSettings => {
  switch (action.type) {
    case "SET_THEME":
      return { ...state, theme: action.payload };
    case "SET_LANGUAGE":
      return { ...state, language: action.payload };
    case "SET_FONT_SIZE":
      return { ...state, fontSize: action.payload };
    case "TOGGLE_ANIMATIONS":
      return { ...state, reduceAnimations: !state.reduceAnimations };
    default:
      return state;
  }
};

const initialState: AppSettings = {
  theme: "light",
  language: "en",
  fontSize: 16,
  reduceAnimations: false,
};

const AppSettingsContext = createContext<{
  settings: AppSettings;
  dispatch: React.Dispatch<Action>;
} | null>(null);

export const AppSettingsProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const [settings, dispatch] = useReducer(settingsReducer, initialState);

  // Persist settings to localStorage
  React.useEffect(() => {
    localStorage.setItem("appSettings", JSON.stringify(settings));
  }, [settings]);

  return (
    <AppSettingsContext.Provider value={{ settings, dispatch }}>
      {children}
    </AppSettingsContext.Provider>
  );
};

export const useAppSettings = () => {
  const context = useContext(AppSettingsContext);
  if (!context) {
    throw new Error("useAppSettings must be used within AppSettingsProvider");
  }
  return context;
};

// components/ThemeToggle.tsx
import React from "react";
import { useAppSettings } from "../context/AppSettingsContext";
import { ThemeMode } from "../types/theme";

export const ThemeToggle: React.FC = () => {
  const { settings, dispatch } = useAppSettings();

  const cycleTheme = () => {
    const themes: ThemeMode[] = ["light", "dark", "reading"];
    const currentIndex = themes.indexOf(settings.theme);
    const nextIndex = (currentIndex + 1) % themes.length;
    dispatch({ type: "SET_THEME", payload: themes[nextIndex] });
  };

  return (
    <button onClick={cycleTheme} className={`theme-btn ${settings.theme}`}>
      Switch to{" "}
      {settings.theme === "light"
        ? "Dark"
        : settings.theme === "dark"
          ? "Reading"
          : "Light"}{" "}
      Mode
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

2. Redux Toolkit (RTK): The Enterprise Standard

Real-World Scenario: A banking application with complex transaction workflows, real-time balance updates, fraud detection alerts, and regulatory compliance requirements.

Why it works here: The banking domain demands absolute predictability, audit trails, time-travel debugging, and complex async workflows. RTK's middleware ecosystem, devtools, and predictable state updates are invaluable.

TypeScript Implementation:

// features/transactions/transactionsSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { Transaction, TransactionFilters, TransactionState } from "./types";
import { bankingAPI } from "../../api/bankingAPI";

const initialState: TransactionState = {
  transactions: [],
  filters: {
    dateRange: { start: null, end: null },
    amountRange: { min: 0, max: 10000 },
    categories: [],
    status: "completed",
  },
  loading: false,
  error: null,
  selectedTransaction: null,
};

// Async thunk for fetching transactions
export const fetchTransactions = createAsyncThunk(
  "transactions/fetchTransactions",
  async (filters: TransactionFilters, { rejectWithValue }) => {
    try {
      const response = await bankingAPI.getTransactions(filters);
      return response.data;
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || "Failed to fetch transactions"
      );
    }
  }
);

// Async thunk for transferring funds
export const transferFunds = createAsyncThunk(
  "transactions/transferFunds",
  async (
    transferData: {
      fromAccount: string;
      toAccount: string;
      amount: number;
      description: string;
    },
    { rejectWithValue }
  ) => {
    try {
      const response = await bankingAPI.transfer(transferData);
      return response.data;
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || "Transfer failed"
      );
    }
  }
);

const transactionsSlice = createSlice({
  name: "transactions",
  initialState,
  reducers: {
    setFilters: (state, action: PayloadAction<Partial<TransactionFilters>>) => {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearFilters: (state) => {
      state.filters = initialState.filters;
    },
    selectTransaction: (state, action: PayloadAction<string>) => {
      state.selectedTransaction =
        state.transactions.find((t) => t.id === action.payload) || null;
    },
  },
  extraReducers: (builder) => {
    builder
      // Fetch transactions cases
      .addCase(fetchTransactions.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTransactions.fulfilled, (state, action) => {
        state.loading = false;
        state.transactions = action.payload;
      })
      .addCase(fetchTransactions.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      })
      // Transfer funds cases
      .addCase(transferFunds.fulfilled, (state, action) => {
        state.transactions.unshift(action.payload);
      })
      .addCase(transferFunds.rejected, (state, action) => {
        state.error = action.payload as string;
      });
  },
});

export const { setFilters, clearFilters, selectTransaction } =
  transactionsSlice.actions;
export default transactionsSlice.reducer;

// features/transactions/TransactionList.tsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../app/store";
import { fetchTransactions, setFilters } from "./transactionsSlice";
import { TransactionFilters } from "./types";

export const TransactionList: React.FC = () => {
  const dispatch = useDispatch();
  const { transactions, filters, loading, error } = useSelector(
    (state: RootState) => state.transactions
  );

  useEffect(() => {
    dispatch(fetchTransactions(filters));
  }, [dispatch, filters]);

  const handleFilterChange = (newFilters: Partial<TransactionFilters>) => {
    dispatch(setFilters(newFilters));
  };

  if (loading)
    return <div className="loading-spinner">Loading transactions...</div>;
  if (error) return <div className="error-message">Error: {error}</div>;

  return (
    <div className="transaction-list">
      <TransactionFilters
        filters={filters}
        onFilterChange={handleFilterChange}
      />
      {transactions.map((transaction) => (
        <TransactionItem key={transaction.id} transaction={transaction} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

3. Zustand: The Minimalist Marvel

Real-World Scenario: A collaborative design tool where multiple users can edit documents simultaneously, with real-time cursor positions and presence indicators.

Why it works here: Zustand's simplicity and performance make it ideal for high-frequency updates like cursor movements. Its small bundle size benefits web applications, and the simple API enables rapid development.

TypeScript Implementation:

// stores/useCollaborationStore.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { User, CursorPosition, DesignDocument } from "../types/collaboration";

interface CollaborationState {
  // Document state
  currentDocument: DesignDocument | null;
  elements: DesignElement[];

  // User presence
  activeUsers: User[];
  userCursors: Record<string, CursorPosition>;

  // Actions
  setCurrentDocument: (doc: DesignDocument) => void;
  updateElement: (elementId: string, updates: Partial<DesignElement>) => void;
  addElement: (element: DesignElement) => void;
  setUserCursor: (userId: string, position: CursorPosition) => void;
  addActiveUser: (user: User) => void;
  removeActiveUser: (userId: string) => void;

  // Derived state
  getElement: (elementId: string) => DesignElement | undefined;
  getUsersInDocument: () => User[];
}

export const useCollaborationStore = create<CollaborationState>()(
  devtools((set, get) => ({
    currentDocument: null,
    elements: [],
    activeUsers: [],
    userCursors: {},

    setCurrentDocument: (doc) => set({ currentDocument: doc }),

    updateElement: (elementId, updates) =>
      set((state) => ({
        elements: state.elements.map((element) =>
          element.id === elementId ? { ...element, ...updates } : element
        ),
      })),

    addElement: (element) =>
      set((state) => ({ elements: [...state.elements, element] })),

    setUserCursor: (userId, position) =>
      set((state) => ({
        userCursors: { ...state.userCursors, [userId]: position },
      })),

    addActiveUser: (user) =>
      set((state) => ({
        activeUsers: state.activeUsers.some((u) => u.id === user.id)
          ? state.activeUsers
          : [...state.activeUsers, user],
      })),

    removeActiveUser: (userId) =>
      set((state) => ({
        activeUsers: state.activeUsers.filter((user) => user.id !== userId),
        userCursors: Object.fromEntries(
          Object.entries(state.userCursors).filter(([id]) => id !== userId)
        ),
      })),

    getElement: (elementId) =>
      get().elements.find((element) => element.id === elementId),

    getUsersInDocument: () =>
      get().activeUsers.filter(
        (user) => user.currentDocumentId === get().currentDocument?.id
      ),
  }))
);

// components/CursorTracker.tsx
import React, { useEffect } from "react";
import { useCollaborationStore } from "../stores/useCollaborationStore";

export const CursorTracker: React.FC = () => {
  const { userCursors, getUsersInDocument } = useCollaborationStore();
  const users = getUsersInDocument();

  return (
    <div className="cursor-tracker">
      {Object.entries(userCursors).map(([userId, position]) => {
        const user = users.find((u) => u.id === userId);
        if (!user) return null;

        return (
          <div
            key={userId}
            className="user-cursor"
            style={{ left: position.x, top: position.y }}
          >
            <div className="cursor-arrow" style={{ color: user.color }} />
            <span className="user-name" style={{ backgroundColor: user.color }}>
              {user.name}
            </span>
          </div>
        );
      })}
    </div>
  );
};

// components/DesignCanvas.tsx
import React from "react";
import { useCollaborationStore } from "../stores/useCollaborationStore";

export const DesignCanvas: React.FC = () => {
  const { elements, updateElement, setUserCursor } = useCollaborationStore();

  const handleCanvasMouseMove = (event: React.MouseEvent) => {
    const rect = event.currentTarget.getBoundingClientRect();
    const cursorPosition = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    };

    // Update local cursor position in store
    setUserCursor("current-user-id", cursorPosition);

    // In a real app, you'd also send this to other collaborators via WebSocket
  };

  return (
    <div className="design-canvas" onMouseMove={handleCanvasMouseMove}>
      {elements.map((element) => (
        <DesignElement
          key={element.id}
          element={element}
          onUpdate={(updates) => updateElement(element.id, updates)}
        />
      ))}
      <CursorTracker />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

4. React Query / TanStack Query: The Data Fetching Maestro

Real-World Scenario: A social media dashboard displaying personalized content feeds, notifications, user profiles, and analytics data from multiple API endpoints.

Why it works here: React Query handles server state synchronization, caching, background updates, and pagination effortlessly. It eliminates the need to manage loading states, error handling, and caching logic manually.

TypeScript Implementation:

// hooks/useSocialData.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { socialAPI } from "../api/socialAPI";
import { Post, UserProfile, Notification } from "../types/social";

// Query keys for consistent caching
export const queryKeys = {
  posts: (filters?: any) => ["posts", filters],
  post: (id: string) => ["post", id],
  profile: (userId: string) => ["profile", userId],
  notifications: ["notifications"],
};

// Custom hook for posts with filters
export const usePosts = (filters: { type?: string; userId?: string } = {}) => {
  return useQuery<Post[], Error>({
    queryKey: queryKeys.posts(filters),
    queryFn: () => socialAPI.getPosts(filters),
    staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
    gcTime: 30 * 60 * 1000, // Keep unused data in cache for 30 minutes
  });
};

// Custom hook for user profile
export const useUserProfile = (userId: string) => {
  return useQuery<UserProfile, Error>({
    queryKey: queryKeys.profile(userId),
    queryFn: () => socialAPI.getUserProfile(userId),
    enabled: !!userId, // Only fetch if userId is available
  });
};

// Custom hook for notifications with real-time updates
export const useNotifications = () => {
  return useQuery<Notification[], Error>({
    queryKey: queryKeys.notifications,
    queryFn: () => socialAPI.getNotifications(),
    refetchInterval: 30000, // Auto-refresh every 30 seconds
  });
};

// Mutation for creating a new post
export const useCreatePost = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postData: { content: string; media?: File }) =>
      socialAPI.createPost(postData),
    onSuccess: (newPost) => {
      // Update the posts cache with the new post
      queryClient.setQueryData<Post[]>(queryKeys.posts(), (oldPosts = []) => [
        newPost,
        ...oldPosts,
      ]);

      // Invalidate and refetch to ensure we have the latest data
      queryClient.invalidateQueries({ queryKey: queryKeys.posts() });
    },
    onError: (error) => {
      console.error("Failed to create post:", error);
      // Show error notification to user
    },
  });
};

// components/NewsFeed.tsx
import React from "react";
import { usePosts, useCreatePost } from "../hooks/useSocialData";
import { PostCard } from "./PostCard";
import { CreatePostForm } from "./CreatePostForm";

export const NewsFeed: React.FC = () => {
  const { data: posts, isLoading, error } = usePosts();
  const createPostMutation = useCreatePost();

  const handleCreatePost = (content: string, media?: File) => {
    createPostMutation.mutate({ content, media });
  };

  if (isLoading) return <div className="loading">Loading posts...</div>;
  if (error) return <div className="error">Error: {error.message}</div>;

  return (
    <div className="news-feed">
      <CreatePostForm onSubmit={handleCreatePost} />

      <div className="posts-container">
        {posts?.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>

      {posts?.length === 0 && (
        <div className="empty-state">
          <p>No posts yet. Be the first to share something!</p>
        </div>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Decision Framework: Choosing the Right Solution for Your 2025 Project

Project Type Recommended Solution Key Considerations
Small Business App Context API + useReducer Simple, no dependencies, built into React. Perfect for low-complexity apps with minimal state updates.
Medium Complexity App Zustand + React Query Excellent performance, minimal boilerplate, great dev experience. Ideal for startups and mid-sized applications.
Enterprise Application Redux Toolkit + RTK Query Predictable state flow, excellent devtools, strong ecosystem. Essential for large teams and complex business logic.
Real-Time Collaborative App Zustand/Recoil + WebSockets Fine-grained updates, minimal re-renders. Perfect for apps with real-time collaboration features.
Data-Intensive Dashboard React Query + Zustand Superior data synchronization, caching, and background updates. Best for dashboards and analytics platforms.
Cross-Platform Mobile App Redux Toolkit or Zustand Consistent state management across iOS and Android. Redux for complex apps, Zustand for simpler ones.

Advanced Patterns for 2025

1. State Machine Integration with XState

For mission-critical workflows (payment processing, onboarding flows), combine your state management solution with XState:

// machines/checkoutMachine.ts
import { createMachine, assign } from "xstate";
import { useMachine } from "@xstate/react";
import { checkoutAPI } from "../api/checkout";

interface CheckoutContext {
  cartItems: CartItem[];
  shippingAddress: Address | null;
  paymentMethod: PaymentMethod | null;
  orderId: string | null;
  error: string | null;
}

type CheckoutEvent =
  | { type: "SET_SHIPPING"; address: Address }
  | { type: "SET_PAYMENT"; method: PaymentMethod }
  | { type: "CONFIRM_ORDER" }
  | { type: "RETRY" };

export const checkoutMachine = createMachine<CheckoutContext, CheckoutEvent>({
  id: "checkout",
  initial: "shipping",
  context: {
    cartItems: [],
    shippingAddress: null,
    paymentMethod: null,
    orderId: null,
    error: null,
  },
  states: {
    shipping: {
      on: {
        SET_SHIPPING: {
          actions: assign({
            shippingAddress: (_, event) => event.address,
          }),
          target: "payment",
        },
      },
    },
    payment: {
      on: {
        SET_PAYMENT: {
          actions: assign({
            paymentMethod: (_, event) => event.method,
          }),
          target: "review",
        },
      },
    },
    review: {
      on: {
        CONFIRM_ORDER: "processing",
      },
    },
    processing: {
      invoke: {
        src: (context) =>
          checkoutAPI.processOrder({
            items: context.cartItems,
            shipping: context.shippingAddress!,
            payment: context.paymentMethod!,
          }),
        onDone: {
          actions: assign({
            orderId: (_, event) => event.data.orderId,
          }),
          target: "success",
        },
        onError: {
          actions: assign({
            error: (_, event) => event.data.message,
          }),
          target: "failure",
        },
      },
    },
    success: { type: "final" },
    failure: {
      on: {
        RETRY: "processing",
      },
    },
  },
});

// components/CheckoutFlow.tsx
export const CheckoutFlow: React.FC = () => {
  const [state, send] = useMachine(checkoutMachine);

  return (
    <div className="checkout-flow">
      {state.matches("shipping") && (
        <ShippingForm
          onSubmit={(address) => send({ type: "SET_SHIPPING", address })}
        />
      )}

      {state.matches("payment") && (
        <PaymentForm
          onSubmit={(method) => send({ type: "SET_PAYMENT", method })}
        />
      )}

      {state.matches("review") && (
        <OrderReview
          context={state.context}
          onConfirm={() => send({ type: "CONFIRM_ORDER" })}
        />
      )}

      {state.matches("processing") && <ProcessingSpinner />}

      {state.matches("failure") && (
        <ErrorDisplay
          error={state.context.error}
          onRetry={() => send({ type: "RETRY" })}
        />
      )}

      {state.matches("success") && (
        <OrderConfirmation orderId={state.context.orderId!} />
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

2. Offline-First Strategy with Zustand and React Query

// stores/useOfflineStore.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { asyncStorage } from "zustand/middleware";

interface OfflineState {
  queue: Array<{
    id: string;
    type: string;
    payload: any;
    timestamp: number;
  }>;
  addToQueue: (action: { type: string; payload: any }) => void;
  processQueue: () => Promise<void>;
  clearProcessed: (ids: string[]) => void;
}

export const useOfflineStore = create<OfflineState>()(
  persist(
    (set, get) => ({
      queue: [],

      addToQueue: (action) => {
        const id = Math.random().toString(36).substr(2, 9);
        set((state) => ({
          queue: [...state.queue, { id, ...action, timestamp: Date.now() }],
        }));
      },

      processQueue: async () => {
        const { queue, clearProcessed } = get();
        const successfulIds: string[] = [];

        for (const item of queue) {
          try {
            // Execute the action based on type
            switch (item.type) {
              case "CREATE_POST":
                await socialAPI.createPost(item.payload);
                break;
              case "UPDATE_PROFILE":
                await socialAPI.updateProfile(item.payload);
                break;
              // Add more action types as needed
            }
            successfulIds.push(item.id);
          } catch (error) {
            console.error(`Failed to process action ${item.id}:`, error);
          }
        }

        clearProcessed(successfulIds);
      },

      clearProcessed: (ids) => {
        set((state) => ({
          queue: state.queue.filter((item) => !ids.includes(item.id)),
        }));
      },
    }),
    {
      name: "offline-queue",
      storage: createJSONStorage(() => asyncStorage),
    }
  )
);

// hooks/useOfflineSync.ts
export const useOfflineSync = () => {
  const { processQueue, queue } = useOfflineStore();
  const queryClient = useQueryClient();

  // Process queue when coming online
  useEffect(() => {
    const handleOnline = () => {
      if (queue.length > 0) {
        processQueue().then(() => {
          // Refetch all queries to sync with server
          queryClient.invalidateQueries();
        });
      }
    };

    window.addEventListener("online", handleOnline);
    return () => window.removeEventListener("online", handleOnline);
  }, [processQueue, queue.length, queryClient]);

  return { pendingActions: queue.length };
};
Enter fullscreen mode Exit fullscreen mode

Conclusion: Architecting for the Future

In 2025, state management is no longer about choosing a single library but about creating a layered architecture that addresses different concerns:

  1. Server State: Use React Query/TanStack Query for data fetching, caching, and synchronization
  2. Client State: Choose Zustand for simplicity or Redux Toolkit for complex applications
  3. Workflow State: Implement XState for finite state machines in critical flows
  4. Persistence Layer: Combine solutions with smart persistence strategies for offline support

The most successful applications will use a combination of these tools, leveraging the strengths of each while maintaining a clean separation of concerns. Remember that the best state management solution is the one that makes your application reliable, maintainable, and enjoyable to develop—not necessarily the one with the most stars on GitHub.

As you build your next application in 2025, consider starting with React Query for server state and Zustand for client state, then scale up to more specialized solutions as your application's complexity demands.

Top comments (0)