Skip to content

Instantly share code, notes, and snippets.

@MonaAghili
Last active May 3, 2025 19:17
Show Gist options
  • Save MonaAghili/df3575cec1f6346d3d047d1d915bc617 to your computer and use it in GitHub Desktop.
Save MonaAghili/df3575cec1f6346d3d047d1d915bc617 to your computer and use it in GitHub Desktop.
I'm facing an issue with the toast message in my React Native app using the react-native-toast-message library. When the keyboard is minimized, the toast stays in the middle of the page, causing a poor user experience.
Problem Details:
I'm dismissing the keyboard when an error occurs using Keyboard.dismiss().
The toast is shown after the keyboard dismissal, but it does not adjust its position when the keyboard is minimized.
I need the toast to appear at the bottom of the screen, but when the keyboard is active or dismissed, it should adapt its position accordingly.
I’ve provided my app’s code, including API and error handling setup. Could you help me refactor the code to ensure the toast appears correctly at the bottom of the screen and behaves well with the keyboard? Additionally, if you have suggestions for improvements in my error handling or toast configuration, I'd appreciate it.
// layout
import { ErrorProvider } from "@/components/error/ErrorProvider";
import { toastConfig } from "@/utils/toastConfig";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "react-native-reanimated";
import Toast from "react-native-toast-message";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
},
},
});
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
if (!loaded) {
// Async font loading only occurs in development.
return null;
}
return (
<QueryClientProvider client={queryClient}>
<ErrorProvider>
<Stack>
<Stack.Screen name="index" options={{ title: "Giphy Explorer" }} />
<Stack.Screen
name="details/[id]"
options={{ title: "GIF Details" }}
/>
<Stack.Screen name="+not-found" />
</Stack>
</ErrorProvider>
<StatusBar style="auto" />
<Toast config={toastConfig} />
</QueryClientProvider>
);
}
// error provider
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ErrorContextType } from './types';
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);
export function ErrorProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<Error | null>(null);
const resetError = () => setError(null);
// Value to be provided
const contextValue: ErrorContextType = {
error,
setError,
resetError,
};
// Render fallback UI or children
if (error) {
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>{error.message || 'an unexpected error occurred'}</Text>
<TouchableOpacity style={styles.button} onPress={resetError}>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return <ErrorContext.Provider value={contextValue}>{children}</ErrorContext.Provider>;
}
// Custom hook to use the error context
export function useError() {
const context = useContext(ErrorContext);
if (context === undefined) {
throw new Error('useError must be used within an ErrorProvider');
}
return context;
}
// a higher order component for try catch pattern
export function withErrorHandling<P extends object>(Component: React.ComponentType<P>) {
return function WithErrorHandling(props: P) {
const { setError } = useError();
try {
return <Component {...props} />;
} catch (error) {
setError(error instanceof Error ? error : new Error('An unknown error occurred'));
return null;
}
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 12,
},
message: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 4,
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
});
// toast config
import { StyleSheet } from "react-native";
import {
BaseToast,
ErrorToast,
InfoToast,
ToastProps,
} from "react-native-toast-message";
export const toastConfig = {
success: (props: ToastProps) => (
<BaseToast
{...props}
style={styles.successToast}
contentContainerStyle={styles.contentContainer}
text1Style={styles.title}
text2Style={styles.message}
/>
),
error: (props: ToastProps) => (
<ErrorToast
{...props}
style={styles.errorToast}
contentContainerStyle={styles.contentContainer}
text1Style={styles.title}
text2Style={styles.message}
/>
),
info: (props: ToastProps) => (
<InfoToast
{...props}
style={styles.infoToast}
contentContainerStyle={styles.contentContainer}
text1Style={styles.title}
text2Style={styles.message}
/>
),
};
const styles = StyleSheet.create({
successToast: {
borderLeftColor: "#4CAF50",
backgroundColor: "#EEFBEF",
height: "auto",
minHeight: 60,
maxWidth: "95%",
width: "95%",
},
errorToast: {
borderLeftColor: "#F44336",
backgroundColor: "#FEECEB",
height: "auto",
minHeight: 60,
maxWidth: "95%",
width: "95%",
},
infoToast: {
borderLeftColor: "#2196F3",
backgroundColor: "#E7F3FE",
height: "auto",
minHeight: 60,
maxWidth: "95%",
width: "95%",
},
contentContainer: {
paddingHorizontal: 15,
paddingVertical: 10,
},
title: {
fontSize: 16,
fontWeight: "600",
color: "#333",
},
message: {
fontSize: 14,
color: "#555",
marginTop: 2,
},
});
//
error utils
/**
* Handles global errors in the application and displays appropriate toast notifications.
*/
import { Keyboard, Platform } from 'react-native';
import Toast from 'react-native-toast-message';
export const handleGlobalError = (error: any) => {
let toastType = 'error';
let title = 'Error';
let message = error?.customMessage || 'Something went wrong';
// Network errors (no response)
if (!error.response && error.request) {
title = 'Network Error';
message = 'Unable to connect. Check your internet connection.';
}
// Server returned a response with an error status
else if (error.response?.status) {
switch (error.response.status) {
case 400:
title = 'Invalid Request';
message = 'The request contains invalid parameters.';
break;
case 401:
case 403:
title = 'Authentication Error';
message = 'API key may be invalid or expired.';
break;
case 404:
toastType = 'info';
title = 'No Results';
message = 'No GIFs found matching your search.';
break;
case 429:
title = 'Too Many Requests';
message = 'Please try again in a moment.';
break;
case 500:
case 502:
case 503:
case 504:
title = 'Server Error';
message = 'Giphy service is currently unavailable.';
break;
default:
break;
}
}
if (error.response?.data?.meta?.msg) {
message = error.response.data.meta.msg;
}
// Check if keyboard is visible
const isKeyboardVisible = Keyboard.isVisible;
// If keyboard is visible, dismiss it first and then show toast with a slight delay
if (isKeyboardVisible()) {
Keyboard.dismiss();
// Wait for keyboard to fully dismiss before showing toast
setTimeout(() => {
showToast(toastType, title, message);
}, Platform.OS === 'ios' ? 300 : 150);
} else {
// Show toast immediately if keyboard is not visible
showToast(toastType, title, message);
}
return error;
};
// Helper function to show toast
const showToast = (type: string, title: string, message: string) => {
Toast.show({
type: type,
text1: title,
text2: message,
position: 'bottom',
visibilityTime: 3000,
bottomOffset: 20,
});
};
// http sender
import { handleGlobalError } from '@/utils/errorUtils';
import axios from 'axios';
import { API_CONFIG } from './APIs';
export const giphyAxios = axios.create({
baseURL: API_CONFIG.URL,
params: {
api_key: API_CONFIG.KEY,
},
});
giphyAxios.interceptors.response.use(
response => response,
error => {
handleGlobalError(error);
return Promise.reject(error);
},
);
//apis
/**
* this objects containing Giphy API endpoint routes.
* It made readonly in order to prevent accidental modifications of API routes and
* ensuring consistent API endpoint usage throughout the entire application.
*/
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
const apiConfigData = {
KEY: process.env.EXPO_PUBLIC_API_KEY,
URL: process.env.EXPO_PUBLIC_API_URL,
};
const giphyRoutesData = {
RANDOM: '/gifs/random',
SEARCH: '/gifs/search',
GIF_BY_ID: (id: string) => `/gifs/${id}`,
};
export const API_CONFIG: ReadOnly<typeof apiConfigData> = apiConfigData;
export const GIPHY_ROUTES: ReadOnly<typeof giphyRoutesData> = giphyRoutesData;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment