Centralized Loading and Error Handling in React Native with Middleware and Zustand
Managing loading states and handling errors efficiently is crucial for creating a smooth user experience in React Native applications. In this blog, we will explore how to centralize loading state management using Redux Toolkit Query (RTK Query) and middleware, while leveraging Zustand for error handling.
1. Centralized Loading Management with RTK Query
RTK Query simplifies API state management by handling caching, invalidation, and background fetching. We can centralize the loading state by selecting the active API requests using a global selector.
Defining a Global Loading Selector
import { createSelector } from '@reduxjs/toolkit';
import { movieApi } from '../../src/redux/query/RTKQuery.ts';
const selectGlobalLoading = createSelector(
(state: any) => state[movieApi.reducerPath],
(apiState) => {
const isFetchingQueries = Object.values(apiState.queries).some(
(query: any) => query?.status === 'pending'
);
const isFetchingMutations = Object.values(apiState.mutations).some(
(mutation: any) => mutation?.status === 'pending'
);
return isFetchingQueries || isFetchingMutations;
}
);
export { selectGlobalLoading };
This selector scans all active API queries and mutations, checking if any are still in a “pending” state.
Creating a Global Loading Spinner
To display a loading indicator whenever an API call is in progress, we use the useSelector
hook.
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
import styles from './LoadingSpinner.style.ts';
import { useSelector } from 'react-redux';
import { selectGlobalLoading } from '../../utils/Common.ts';
import { colors } from '../../constant/Colors.ts';
const LoadingSpinner = () => {
const isLoading = useSelector(selectGlobalLoading);
if (!isLoading) return null;
return (
<View style={styles.container}>
<ActivityIndicator size={'large'} color={colors.primaryColor} />
</View>
);
};export default LoadingSpinner;
This component listens for loading state updates and displays an overlay with an ActivityIndicator
when an API request is in progress.
Adding the Loading Spinner to the App
We integrate the LoadingSpinner
component into the main application layout:
<SafeAreaProvider>
<Navigation />
<NetworkConnection />
<LoadingSpinner />
</SafeAreaProvider>
This ensures that the loading indicator is always accessible at the root level of the app.
2. Centralized Error Handling Using Middleware and Zustand
Instead of manually handling API errors in each component, we can use Redux middleware to capture errors globally and manage them using Zustand.
Defining Zustand Store for Error Handling
Zustand is a lightweight state management library that provides a simple API for managing global state.
import { create } from 'zustand';
interface ErrorState {
visible: boolean;
message: string;
showError: (message: string) => void;
hideError: () => void;
}export const useApiErrorStore = create<ErrorState>((set) => ({
visible: false,
message: '',
showError: (message: string) => set({ visible: true, message }),
hideError: () => set({ visible: false, message: '' }),
}));
This store exposes methods to show and hide error messages globally.
Creating an RTK Middleware for Error Handling
Middleware allows us to intercept Redux actions and handle errors in a centralized way.
import { isRejectedWithValue, Middleware } from '@reduxjs/toolkit';
import { useApiErrorStore } from '../zustand-store/ApiErrorStore.ts';
interface ErrorResponse {
status: number;
data: {
message?: string;
error?: string;
};
}export const rtkQueryErrorMiddleware: Middleware = () => (next) => (action) => {
if (isRejectedWithValue(action)) {
const error = action.payload as ErrorResponse;
const errorMessage =
error.data?.message ||
error.data?.error ||
'Something went wrong. Please try again.';
if (errorMessage !== 'Invalid token')
useApiErrorStore.getState().showError(errorMessage);
}
return next(action);
};
This middleware listens for rejected API requests and automatically updates the Zustand store with the error message.
Integrating Middleware in Redux Store Configuration
We add the error-handling middleware to the Redux store:
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import { movieApi } from './query/RTKQuery.ts';
import { setupListeners } from '@reduxjs/toolkit/query';
import { rtkQueryErrorMiddleware } from './RtkQueryErrorMiddleware.ts';
const configureAppStore = () => {
const store = configureStore({
reducer: {
[movieApi.reducerPath]: movieApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat([
logger,
rtkQueryErrorMiddleware,
movieApi.middleware,
]),
devTools: process.env.NODE_ENV === 'development',
});
setupListeners(store.dispatch);
return store;
};
export default configureAppStore;
This ensures that API errors are caught and processed before they reach UI components.
Conclusion
By implementing a centralized loading state using RTK Query and Redux, combined with middleware-driven error handling and Zustand, we have built a robust foundation for managing API state in a React Native app. This approach improves maintainability and ensures a smoother user experience by automatically handling loading indicators and displaying error messages globally.
GitHub: https://github.com/piashcse/react-native-movie
Blog: https://piashcse.blogspot.com/2025/02/centralized-loading-and-error-handling.html