Mehedi Hassan Piash | Senior Software Engineer | Android | iOS | KMP | Ktor | Jetpack Compose | React-Native.

February 16, 2025

Centralized Loading and Error Handling in React Native with Middleware and Zustand

February 16, 2025 Posted by Piash , , , , No comments

 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.

Centralized Loading Management with RTK Query

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
Medium: https://piashcse.medium.com/centralized-loading-and-error-handling-in-react-native-with-middleware-and-zustand-683efd02b792

January 17, 2025

Authenticator in Axios with RTK Query in a React Native Application

January 17, 2025 Posted by Piash , , , No comments

 In modern applications, implementing secure and seamless authentication mechanisms is crucial. One common approach is using access tokens with a refresh token mechanism. This article demonstrates how to integrate an automatic token refresh workflow using Axios, RTK Query, and Zustand for state management in a React Native app.

Axios authenticator with RTK Query

Overview of the Implementation

• Axios Interceptors: Intercepts requests and responses to add authentication tokens and handle token expiration.

• Zustand: Manages the authentication state, including storing and updating tokens.

• RTK Query: Handles API calls, including a mutation for refreshing the token.

Let’s start step by step…

1. Prerequisites

Before we dive into the code, ensure you have the required dependencies installed in your project. Use the following commands to install the necessary packages:

npm install @reduxjs/toolkit react-redux axios axios-logger @react-native-async-storage/async-storage zustand

2. Configuring Axios with Token Handling

Axios interceptors are used to add the token to outgoing requests and handle token expiration by refreshing it.

AxiosInstance.ts

import axios from 'axios';
import * as AxiosLogger from 'axios-logger';
import { expoMuseumApi } from '@/src/redux/RTKQuery';
import { useAppStore } from '@/src/zustand/useAppStore';
import configureAppStore from '@/src/store/Store';

const axiosInstance = axios.create();
axiosInstance.interceptors.request.use((config) => {
const token = useAppStore.getState().token;
// Add bearer token to headers
config.headers.Authorization = `Bearer ${token}`;
// Keep the original logger
AxiosLogger.requestLogger(config);
console.log('Authorization header', config.headers);
return config;
}, AxiosLogger.errorLogger);

axiosInstance.interceptors.response.use(
AxiosLogger.responseLogger,
AxiosLogger.errorLogger
);

axiosInstance.interceptors.response.use(
AxiosLogger.responseLogger, // Log response
async (error) => {
const originalRequest = error.config;

// Handle 401 (Unauthorized) errors
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // Prevent infinite retry loops

try {
// Trigger the RTK Query `postRefreshToken` mutation
const { data } = await configureAppStore().dispatch(
expoMuseumApi.endpoints.postRefreshToken.initiate()
);
// Update token in Zustand store
useAppStore.getState().saveToken(data as string);

// Update Authorization header with the new token
originalRequest.headers.Authorization = `Bearer ${useAppStore.getState().getToken()}`;

// Retry the original request
return axiosInstance(originalRequest);
} catch (refreshError) {
console.error('Failed to refresh token:', refreshError);
// Clear user data and token in Zustand store on failure
useAppStore.getState().clearToken();
//TODO [Navigate to login screen]

return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

export default axiosInstance;

3. Setting Up Zustand for Token Management

We use Zustand to manage the token state. The token is persisted locally using AsyncStorage to ensure it’s retained across app sessions.

useAppStore.ts

import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface zustandConfig {
token: string;
saveToken: (token: string) => void;
getToken: () => string;
clearToken: () => void;
}

export const useAppStore = create<zustandConfig>()(
persist(
(set, get) => ({
token: '',
saveToken: (token: string) => set({ token }),
getToken: () => get().token,
clearToken: () => set({ token: '' }),
}),
{
name: '@expoMuseum',
storage: createJSONStorage(() => AsyncStorage),
}
)
);

4. RTK Query with Axios Base Query

Define a base query using Axios to integrate it with RTK Query.

BaseQuery.ts

import { AxiosError, AxiosRequestConfig } from 'axios';
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
import axiosInstance from '@/src/redux/AxiosInstance';
import { AppConstants } from '@/src/constants/AppConstant';

type AxiosBaseQueryArgs = {
baseUrl?: string;
};

type AxiosQueryParams = {
url: string;
method?: AxiosRequestConfig['method'];
data?: AxiosRequestConfig['data'];
params?: AxiosRequestConfig['params'];
headers?: AxiosRequestConfig['headers'];
};

const axiosBaseQuery =
(
{ baseUrl }: AxiosBaseQueryArgs = { baseUrl: AppConstants.BASE_URL }
): BaseQueryFn<AxiosQueryParams, unknown, unknown> =>
async ({ url, method = 'GET', data, params, headers }) => {
try {
const result = await axiosInstance({
url: baseUrl + url,
method,
data,
params,
headers,
});
return { data: result.data };
} catch (axiosError) {
const error = axiosError as AxiosError;
return {
error: {
status: error.response?.status || 500,
data: error.response?.data || error.message,
},
};
}
};

export default axiosBaseQuery;

5. RTK Query API Definition

Define an API slice that includes a mutation for refreshing tokens and other queries.

RTKQuery.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { useAppStore } from '@/src/zustand/useAppStore';
import axiosBaseQuery from '@/src/redux/BaseQuery';

export const expoMuseumApi = createApi({
reducerPath: 'expoMuseumApi',
baseQuery: axiosBaseQuery(),
endpoints: (builder) => ({
postRefreshToken: builder.mutation<string, void>({
query: () => ({
url: 'auth/refreshToken',
method: 'POST',
headers: {
// Pass the refreshToken in the header while calling refresh token api
Authorization: `Bearer ${useAppStore().token}`,
},
}),
transformResponse: (response: string) => response,
}),
}),
});

export const { usePostRefreshTokenMutation } = expoMuseumApi;

6. Setting Up Redux Store

To integrate RTK Query into your app, you need to configure the Redux store and attach the API middleware. This step ensures that RTK Query manages the caching and lifecycle of API calls.

Store.ts

import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { expoMuseumApi } from '@/src/redux/RTKQuery';

const configureAppStore = () => {
const store = configureStore({
reducer: {
[expoMuseumApi.reducerPath]: expoMuseumApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
}).concat([expoMuseumApi.middleware]),
devTools: process.env.NODE_ENV === 'development',
});
setupListeners(store.dispatch);
return store;
};
export default configureAppStore;

Ref: https://github.com/piashcse/expo-museum
Medium: https://piashcse.medium.com/authenticator-in-axios-with-rtk-query-in-a-react-native-application-c9610c8e288d