Complete guide to using empty states, error states, loading states, success states, and confirmation dialogs in the Marriott Voyage app.
- Empty States
- Error States
- Inline Errors
- Loading States
- Success States
- Confirmation Dialogs
- Best Practices
Empty states appear when there's no content to display. They guide users on what to do next.
import { EmptyState } from '@/components/states';
<EmptyState
icon="📝"
title="No posts yet"
description="Share your travel experiences"
buttonText="Create Post"
onButtonPress={() => navigation.navigate('CreatePost')}
/>import { NoFriendsEmpty } from '@/components/states';
<NoFriendsEmpty
onButtonPress={() => navigation.navigate('FindFriends')}
/>Icon: 👥 Title: "No friends yet" Description: "Start connecting with fellow travelers" Button: "Find Friends"
<NoMessagesEmpty
onButtonPress={() => navigation.navigate('NewMessage')}
/>Icon: 💬 Title: "No messages" Description: "Your conversations will appear here"
<NoBookingsEmpty
onButtonPress={() => navigation.navigate('Events')}
/>Icon: 🎟️ Title: "No bookings yet" Description: "Discover events and activities"
<NoPostsEmpty
onButtonPress={() => navigation.navigate('CreatePost')}
/>Icon: 📝 Title: "No posts yet" Description: "Share your travel experiences"
<SearchNoResults
onButtonPress={() => setSearchQuery('')}
/>Icon: 🔍 Title: "No results found" Description: "Try a different search term"
NoNotificationsEmpty- No notificationsNoPlansEmpty- No plans createdNoMatchesEmpty- No traveler matchesNoReviewsEmpty- No reviews yetNoPhotosEmpty- No photos uploadedNoBadgesEmpty- No badges earnedNoSavedItemsEmpty- No saved items
Error states appear when something goes wrong. They explain the issue and offer recovery options.
import { ErrorState } from '@/components/states';
<ErrorState
icon="⚠️"
title="Something went wrong"
description="We're working on fixing this"
buttonText="Try Again"
onButtonPress={handleRetry}
variant="error" // 'error' | 'warning' | 'info'
/>import { NetworkError } from '@/components/states';
<NetworkError onRetry={refetch} />Icon: 📡 Title: "No internet connection" Description: "Check your connection and try again"
<GenericError onRetry={refetch} />Icon:
<NotFoundError onRetry={() => navigation.goBack()} />Icon: 🤷 Title: "Page not found" Description: "This content may have been removed"
ServerError- Server issuesPermissionDeniedError- Access deniedTimeoutError- Request timeoutMaintenanceError- Under maintenanceLocationPermissionError- Location access requiredCameraPermissionError- Camera access requiredAgeRestrictedError- Age restricted content
Inline errors appear within forms and UI elements for immediate feedback.
For form field validation errors:
import { FieldError } from '@/components/states';
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
/>
<FieldError
message="Please enter a valid email address"
visible={!!emailError}
/>For important errors at the top of the screen:
import { BannerError } from '@/components/states';
<BannerError
message="Failed to save changes. Please try again."
visible={showError}
/>For non-critical warnings:
import { InlineWarning } from '@/components/states';
<InlineWarning
message="Your profile is incomplete. Complete it to get more matches."
visible={!isProfileComplete}
/>For API/network errors:
import { ApiError } from '@/components/states';
<ApiError
message="Failed to load events. Please check your connection and try again."
visible={!!apiError}
/>For multiple form errors:
import { ValidationSummary } from '@/components/states';
<ValidationSummary
errors={[
'Name is required',
'Email must be valid',
'Password must be at least 8 characters'
]}
visible={errors.length > 0}
/>Loading states show progress and keep users informed during operations.
For initial app loading or major operations:
import { FullScreenLoader } from '@/components/states';
{isLoading && (
<FullScreenLoader message="Loading your profile..." />
)}For small loading indicators:
import { InlineLoader } from '@/components/states';
<InlineLoader message="Sending..." />For blocking operations with semi-transparent overlay:
import { LoadingOverlay } from '@/components/states';
{isProcessing && (
<LoadingOverlay message="Processing payment..." />
)}For FlatList pagination:
import { LoadingMoreFooter } from '@/components/states';
<FlatList
data={items}
renderItem={renderItem}
ListFooterComponent={
isLoadingMore ? <LoadingMoreFooter /> : null
}
/>For pull-to-refresh:
import { RefreshingHeader } from '@/components/states';
<FlatList
data={items}
renderItem={renderItem}
refreshing={isRefreshing}
onRefresh={handleRefresh}
ListHeaderComponent={
isRefreshing ? <RefreshingHeader /> : null
}
/>For content placeholder while loading:
import { ContentLoader } from '@/components/states';
{isLoading ? (
<ContentLoader />
) : (
<View>
<Text>{content}</Text>
</View>
)}For file uploads with progress:
import { UploadingProgress } from '@/components/states';
<UploadingProgress
progress={uploadProgress} // 0-100
message="Uploading photo..."
/>For operations like image processing:
import { ProcessingLoader } from '@/components/states';
{isProcessing && (
<ProcessingLoader message="Processing image..." />
)}Success states celebrate user achievements and provide positive feedback.
For successful operations:
import { SuccessModal } from '@/components/states';
<SuccessModal
visible={showSuccess}
title="Booking Confirmed!"
message="Check your email for details"
buttonText="View Booking"
onClose={() => {
setShowSuccess(false);
navigation.navigate('Bookings');
}}
autoClose={false}
/>With auto-close:
<SuccessModal
visible={showSuccess}
title="Message Sent"
onClose={() => setShowSuccess(false)}
autoClose={true}
autoCloseDelay={2000}
/>For inline success messages:
import { InlineSuccess } from '@/components/states';
<InlineSuccess
message="Profile updated successfully"
visible={showSuccess}
/>For gamification and rewards:
import { PointsEarned } from '@/components/states';
<PointsEarned
visible={showPoints}
points={50}
reason="Completed your profile"
onClose={() => setShowPoints(false)}
/>For badge achievements:
import { BadgeEarned } from '@/components/states';
<BadgeEarned
visible={showBadge}
badgeName="Social Butterfly"
badgeIcon="🦋"
description="Made 10 new friends"
onClose={() => setShowBadge(false)}
/>Confirmation dialogs ask users to confirm important or destructive actions.
import { ConfirmationDialog } from '@/components/states';
<ConfirmationDialog
visible={showDialog}
title="Delete Post"
message="Are you sure you want to delete this post?"
confirmText="Delete"
cancelText="Cancel"
onConfirm={handleDelete}
onCancel={() => setShowDialog(false)}
variant="danger" // 'danger' | 'warning' | 'info'
icon="📝"
/>For native platform dialogs:
import { showNativeConfirmation } from '@/components/states';
showNativeConfirmation({
title: 'Log Out',
message: 'Are you sure you want to log out?',
confirmText: 'Log Out',
cancelText: 'Cancel',
onConfirm: handleLogout,
onCancel: () => {},
});The ConfirmationDialogs object provides pre-configured confirmations:
import { ConfirmationDialogs } from '@/components/states';
ConfirmationDialogs.removeFriend(
'John Doe',
() => handleRemoveFriend(),
() => {}
);ConfirmationDialogs.cancelBooking(
() => handleCancelBooking(),
() => {}
);ConfirmationDialogs.deletePost(
() => handleDeletePost(),
() => {}
);ConfirmationDialogs.logout(
() => handleLogout(),
() => {}
);ConfirmationDialogs.blockUser(
'Jane Smith',
() => handleBlockUser(),
() => {}
);ConfirmationDialogs.leavePlan(
'Beach Volleyball',
() => handleLeavePlan(),
() => {}
);ConfirmationDialogs.deleteAccount(
() => handleDeleteAccount(),
() => {}
);ConfirmationDialogs.discardChanges(
() => handleDiscard(),
() => {}
);- Always provide a CTA: Guide users on what to do next
- Use friendly, encouraging language: "Start connecting" instead of "No data"
- Add relevant icons: Make empty states visually appealing
- Context matters: Explain why the state is empty
- Be specific: Tell users exactly what went wrong
- Offer solutions: Provide retry buttons or alternative actions
- Use appropriate variants: danger for errors, warning for caution, info for FYI
- Don't blame the user: "Connection failed" instead of "You're offline"
- Show progress when possible: Use progress bars for uploads
- Keep users informed: Add descriptive messages
- Don't overuse: Only show loading for operations >300ms
- Use skeletons for content: Better UX than spinners
- Celebrate achievements: Use animations and haptics
- Be brief: Auto-close for minor successes
- Provide next steps: Guide users after success
- Make it feel special: Use confetti, animations for big wins
- Only for important actions: Don't overuse confirmations
- Be clear about consequences: Explain what will happen
- Use destructive style for dangerous actions: Red/danger variant
- Provide clear options: "Delete" vs "Cancel", not "Yes" vs "No"
function ProfileForm() {
const [name, setName] = useState('');
const [errors, setErrors] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const handleSubmit = async () => {
// Validation
const validationErrors = [];
if (!name) validationErrors.push('Name is required');
if (validationErrors.length > 0) {
setErrors(validationErrors);
return;
}
// Loading
setIsLoading(true);
try {
await updateProfile({ name });
// Success
setIsLoading(false);
setShowSuccess(true);
} catch (error) {
// Error
setIsLoading(false);
showErrorToast({
title: 'Failed to update profile',
message: error.message,
});
}
};
return (
<View>
{/* Validation errors */}
<ValidationSummary errors={errors} visible={errors.length > 0} />
{/* Form field */}
<TextInput
value={name}
onChangeText={setName}
placeholder="Name"
/>
<FieldError
message="Name is required"
visible={errors.includes('Name is required')}
/>
{/* Submit button */}
<AnimatedButton
title="Save"
onPress={handleSubmit}
loading={isLoading}
/>
{/* Success modal */}
<SuccessModal
visible={showSuccess}
title="Profile Updated"
message="Your changes have been saved"
onClose={() => setShowSuccess(false)}
autoClose={true}
/>
</View>
);
}function EventsList() {
const { data, loading, error, refetch, fetchMore } = useEvents();
if (loading && !data) {
return <FullScreenLoader message="Loading events..." />;
}
if (error) {
return <NetworkError onRetry={refetch} />;
}
if (data?.length === 0) {
return (
<NoBookingsEmpty
onButtonPress={() => navigation.navigate('CreateEvent')}
/>
);
}
return (
<FlatList
data={data}
renderItem={({ item }) => <EventCard event={item} />}
refreshing={loading}
onRefresh={refetch}
onEndReached={fetchMore}
ListFooterComponent={
loading ? <LoadingMoreFooter /> : null
}
/>
);
}Ensure react-native-reanimated is installed and configured:
npx expo install react-native-reanimatedAdd to babel.config.js:
plugins: [
'react-native-reanimated/plugin', // Must be last
],Use showNativeConfirmation for iOS/Android instead of custom modal:
if (Platform.OS !== 'web') {
showNativeConfirmation({...});
}Add flex: 1 to parent container:
<View style={{ flex: 1 }}>
<NoFriendsEmpty />
</View>