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

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

December 19, 2024

Localization in React Native

December 19, 2024 Posted by Piash , , 1 comment

 Localization is essential for making your React Native app accessible to a global audience. By supporting multiple languages, you ensure users from different regions have a seamless experience. Let’s start step by step.

Multiple language support

Step 1: Install Required Libraries

npm install i18next react-i18next react-native-localize

Step 2: Create Translation Files

Store translations in JSON files for each language. For example:
en.json (English):

{
"welcome": "Welcome",
"greeting": "Hello, {{name}}!"
}

bn.json (Bangali):

{
"welcome": "স্বাগতম",
"greeting": "হ্যালো, {{name}}!"
}

Step 3: Initialize i18next

import i18n from 'i18next';
import en from './locale-json/en.json';
import bn from './locale-json/bn.json';
import { initReactI18next } from 'react-i18next';
import { getLocales } from 'react-native-localize';

// Define the type for supported languages
export type TranslationKeys = keyof typeof en;

// Detect the device language
const getDeviceLanguage = (): string => {
const locales = getLocales();
return locales[0]?.languageTag || 'en';
};

// Initialize i18next
i18n.use(initReactI18next).init({
compatibilityJSON: 'v4',
resources: {
en: { translation: en },
bn: { translation: bn },
},
lng: getDeviceLanguage(),
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});

export const i18nLocale = i18n;

Step 4: Wrap your app.js with the provider

import { I18nextProvider } from 'react-i18next';
import i18n from 'i18next';

const App = () => {
return (
<Provider store={store}>
<PaperProvider>
<I18nextProvider i18n={i18n}>
<SafeAreaProvider>
<Navigation />
</SafeAreaProvider>
</I18nextProvider>
</PaperProvider>
</Provider>
);
};

Step 5: Make a hook to make it more useful

import { useMemo } from 'react';
import { i18nLocale } from '../locale/i18nLocale.ts';
import { AppString } from '../locale/AppString.ts';

type AppStringKey = keyof typeof AppString; // Keys of APP_STRING

export const useLocalization = () => {
return useMemo(() => {
const localizedStrings: Record<AppStringKey, string> = {} as Record<
AppStringKey,
string
>;

(Object.keys(AppString) as AppStringKey[]).forEach((key) => {
localizedStrings[key] = i18nLocale.t(AppString[key]);
});

return localizedStrings;
}, []);
};
export const AppString = {
WELCOME: 'Welcome',
GEETINGS: "Hello, {{name}}!"
}

Step 6: Use it in your component

const App = () => {
const localization = useLocalization();

return (
<View>
<Text>{localization.WELCOME}</Text>
</View>
);
};

export default App;

Ref:
GitHub: https://github.com/piashcse/react-native-movie
Medium: https://piashcse.medium.com/localization-in-react-native-a2a48e45cd27

November 23, 2024

Zustand Simplified: Effortless State Management for Favorites in React Native

November 23, 2024 Posted by Piash , , , , No comments

 State management is a cornerstone of any React Native application. While libraries like Redux are popular, sometimes we need something lightweight yet powerful. Enter Zustand, a minimalist state management library that’s perfect for handling state in React Native. In this article, we’ll explore how to use Zustand to manage a favorites list for Movies and TV series in a React Native app.

zustand in react native

Why Use Zustand in React Native?
Zustand is simple, scalable, and performant. Here’s why it’s a great choice for React Native:
1. Minimal Boilerplate: Create stores without cumbersome setup.
2. Middleware Support: Includes built-in middleware for features like persistence.
3. Lightweight: Tiny bundle size and fast performance.
4. Reactivity: Automatically triggers re-renders when state changes.
5. Integration with MMKV Storage: Perfect for persisting data efficiently in React Native.

Setting Up Zustand in React Native
First, ensure you have the required packages installed:

npm install zustand
npm install react-native-mmkv zustand/middleware

Here’s how to set up a Zustand store for managing favorite Movies and TV series:
FavoriteStore.ts

import { create } from 'zustand';
import { MovieDetail } from '../types/MovieDetail.ts';
import { persist, createJSONStorage } from 'zustand/middleware';
import { TvSeriesDetail } from '../types/TvSeriesDetail.ts';
import { zustandMMKVStorage } from './mmkv.ts';

interface FavoriteMoviesTvSeriesStore {
favoriteMovies: MovieDetail[];
toggleFavoriteMovie: (movie: MovieDetail) => void;
isFavoriteMovie: (movieId: number) => boolean;
clearFavoriteMovies: () => void;
favoriteTvSeries: TvSeriesDetail[];
toggleFavoriteTvSeries: (movie: TvSeriesDetail) => void;
isFavoriteTvSeries: (tvSeriesId: number) => boolean;
clearFavoriteTvSeries: () => void;
}

export const useFavoriteStore = create<FavoriteMoviesTvSeriesStore>()(
persist(
(set, get) => ({
favoriteMovies: [],
toggleFavoriteMovie: (movie: MovieDetail) => {
set((state: FavoriteMoviesTvSeriesStore) => {
const isFav = state.favoriteMovies.some(
(favMovie: MovieDetail) => favMovie.id === movie.id
);
return {
favoriteMovies: isFav
? state.favoriteMovies.filter(
(favMovie: MovieDetail) => favMovie.id !== movie.id
) // Remove from favorites
: [...state.favoriteMovies, movie], // Add to favorites
};
});
},
isFavoriteMovie: (movieId: number) =>
get().favoriteMovies.some((movie: MovieDetail) => movie.id === movieId),

clearFavoriteMovies: () => set({ favoriteMovies: [] }),
favoriteTvSeries: [],
toggleFavoriteTvSeries: (tvSeries: TvSeriesDetail) => {
set((state: FavoriteMoviesTvSeriesStore) => {
const isFav = state.favoriteTvSeries.some(
(favTvSeries: TvSeriesDetail) => favTvSeries.id === tvSeries.id
);
return {
favoriteTvSeries: isFav
? state.favoriteTvSeries.filter(
(favTvSeries: TvSeriesDetail) =>
favTvSeries.id !== tvSeries.id
) // Remove from favorites
: [...state.favoriteTvSeries, tvSeries], // Add to favorites
};
});
},
isFavoriteTvSeries: (tvSeriesId: number) =>
get().favoriteTvSeries.some(
(tvSeries: TvSeriesDetail) => tvSeries.id === tvSeriesId
),
clearFavoriteTvSeries: () => set({ favoriteTvSeries: [] }),
}),
{
name: '@favorite',
storage: createJSONStorage(() => zustandMMKVStorage),
}
)
);

MMKV Integration: The zustandMMKVStorage integrates Zustand with react-native-mmkv, a high-performance storage solution. Here’s a simple MMKV wrapper:
MMKV Wrapper (mmkv.ts)

import { MMKV } from 'react-native-mmkv';

const mmkv = new MMKV();

export const zustandMMKVStorage = {
getItem: (name: string) => {
const value = mmkv.getString(name);
return value ? JSON.parse(value) : null;
},
setItem: (name: string, value: any) => {
mmkv.set(name, JSON.stringify(value));
},
removeItem: (name: string) => {
mmkv.delete(name);
},
};

Features Explained

  1. Add/Remove Favorite Movies or TV Series: Use toggleFavoriteMovie and toggleFavoriteTvSeries to add or remove items based on their presence in the list.
    2. Check if an Item is a Favorite: Use isFavoriteMovie or isFavoriteTvSeries to check whether a specific item is in the favorites list.
    3. Clear Favorites: Use clearFavoriteMovies and clearFavoriteTvSeries to reset the lists.
    4. Persistence with MMKV: The store is persisted using MMKV for fast, efficient local storage.

Using the Store in Components

import React, { useState } from 'react';
import { FlatList, View } from 'react-native';
import styles from './FavoriteMovie.style.ts';
import { useFavoriteStore } from '../../../local-store/FavoriteStore.ts';
import FavoriteComponent from '../../../components/favorite/FavoriteComponent.tsx';
import { MovieDetail } from '../../../types/MovieDetail.ts';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParam } from '../../../types/navigation/NavigationTypes.ts';
import ConfirmationAlert from '../../../components/alert-dialog/ConfirmationAlert.tsx';
import { confirmationAlert } from '../../../constant/Dictionary.ts';

type FavoriteMovieNavigationProp = NavigationProp<
RootStackParam,
'MovieDetail'
>;

const FavoriteMovie = () => {
const navigation = useNavigation<FavoriteMovieNavigationProp>();
const { favoriteMovies, toggleFavoriteMovie } = useFavoriteStore();

const [visible, setVisible] = useState(false);
const [movieToRemove, setMovieToRemove] = useState<MovieDetail | null>(null);

const showDialog = (movie: MovieDetail) => {
setMovieToRemove(movie);
setVisible(true);
};

const hideDialog = () => {
setVisible(false);
setMovieToRemove(null);
};

const confirmRemoveFavorite = () => {
if (movieToRemove) {
toggleFavoriteMovie(movieToRemove);
}
hideDialog();
};

const favorite = ({ item }: { item: MovieDetail }) => (
<FavoriteComponent
title={item.title}
poster_path={item.poster_path}
onPress={() => navigation.navigate('MovieDetail', { movieId: item.id })}
onRemove={() => showDialog(item)}
/>
);

return (
<View style={styles.mainView}>
<FlatList
style={styles.flatListStyle}
data={favoriteMovies}
renderItem={favorite}
keyExtractor={(item) => item.id.toString()} // Ensure a unique key for each item
/>
<ConfirmationAlert
visible={visible}
title={confirmationAlert.title}
message={confirmationAlert.message}
onConfirm={confirmRemoveFavorite}
onCancel={hideDialog}
/>
</View>
);
};

export default FavoriteMovie;

GitHub link: https://github.com/piashcse/react-native-movie
Medium: https://piashcse.medium.com/zustand-simplified-effortless-state-management-for-favorites-in-react-native-0b51af8b323a