A modern React application for managing digital wallets and transactions with robust error handling, form validation, and optimized performance.
- Architecture Overview
- Features
- Tech Stack
- Project Structure
- Key Components
- Form Management
- API Integration
- Error Handling
- Performance Optimizations
The application follows a modular architecture with clear separation of concerns:
src/
├── common/ # Shared utilities, components, and hooks
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── services/ # API and service layer
│ └── utils/ # Utility functions
├── modules/ # Feature-based organization
│ ├── wallets/ # Wallet management module
└── pages/ # Route components
├── wallet/ # wallet route
├── transactions/ # transactions route
- Create and manage digital wallets
- Real-time balance updates
- Transaction history with sorting and filtering
- Wallet selection and context management
- Persistent wallet sessions using localStorage
Example of wallet dashboard component:
export const WalletDashboard: React.FC = () => {
const { wallet, isLoading, error } = useWallet();
if (error) {
return <ErrorDisplay message={error} />;
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{isLoading ? <WalletBalanceSkeleton /> : <WalletBalance wallet={wallet} />}
<WalletTransactionForm />
</div>
);
};Example of wallet persistence:
export const useWalletState = () => {
// Initialize wallet ID from localStorage
const [walletId, setWalletIdState] = useState<string | null>(() =>
localStorage.getItem(WALLET_STORAGE_KEY)
);
const setWalletId = useCallback(async (id: string) => {
setWalletIdState(id);
// Persist wallet ID to localStorage
localStorage.setItem(WALLET_STORAGE_KEY, id);
const success = await loadWallet(id);
if (!success) {
clearWallet();
}
}, [loadWallet]);
const clearWallet = useCallback(() => {
setWalletIdState(null);
// Clear wallet data from localStorage on logout/errors
localStorage.removeItem(WALLET_STORAGE_KEY);
}, []);
};- Create new transactions (deposit/withdrawal)
- Transaction type selection with visual feedback
- Amount validation with minimum balance checks
- Transaction history with pagination
- Export functionality
- React - UI library
- TypeScript - Type-safe development
- Vite - Build tool and dev server
- React Router v6 - Client-side routing
- React Context - Global state management
- React Hook Form - Form state management and validation
- @hookform/resolvers - Form validation resolvers
- Zod - Schema validation and type inference
- TailwindCSS - Utility-first CSS framework
- React Hot Toast - Toast notifications
- Axios - HTTP client
- AbortController - Request cancellation
- ESLint - Code linting
- PostCSS - CSS processing
- TypeScript ESLint - TypeScript-specific linting
Example of transaction form implementation:
export const WalletTransactionForm: React.FC = () => {
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors, isSubmitting }
} = useForm<TransactionInput>({
resolver: zodResolver(transactionSchema),
defaultValues: {
amount: MIN_BALANCE,
description: '',
type: TransactionType.CREDIT
}
});
const onSubmit = async (data: TransactionInput) => {
const finalAmount = data.type === TransactionType.DEBIT ? -data.amount : data.amount;
await walletService.transact(walletId, finalAmount, data.description);
};
};Using Zod for type-safe schema validation:
export const transactionSchema = z.object({
amount: z
.number({
required_error: 'Amount is required',
invalid_type_error: 'Please enter a valid amount'
})
.min(MIN_BALANCE, `Amount must be at least ${MIN_BALANCE}`),
description: z
.string()
.min(1, 'Description is required')
.max(100, 'Description must be less than 100 characters'),
type: z.nativeEnum(TransactionType)
});- Request cancellation on unmount
- Pagination with load more button
- Sort and filter state management
-
Error Handling
- Global error boundary for React errors
- Structured API error handling with error codes
- Form validation errors with user feedback
- Toast notifications for success/error states
// Global Error Boundary export class ErrorBoundary extends Component<Props, State> { public static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } public render() { if (this.state.hasError) { return ( <div className="min-h-[400px] flex items-center justify-center"> <div className="text-center p-8"> <h2 className="text-2xl font-bold text-gray-900 mb-4"> Something went wrong </h2> <p className="text-gray-600 mb-6"> {this.state.error?.message || 'An unexpected error occurred'} </p> <Button onClick={this.handleReset}>Try Again</Button> </div> </div> ); } return this.props.children; } } // Structured API Error Handling export const handleApiError = (error: any): void => { if (error?.response?.data?.error?.code) { const code = error.response.data.error.code as ErrorCode; const mapping = ERROR_MAPPINGS[code]; if (mapping) { if (code === ERROR_MAPPINGS.VALIDATION_ERROR.code && error.response?.data?.message) { toast.error(error.response.data.message); return; } toast[mapping.toastType](mapping.message); return; } } toast.error('An unexpected error occurred. Please try again.'); };
-
Loading States
- Skeleton loaders for initial page load
- Loading overlays for operations
- Disabled states during form submission
- Progress indicators for long operations
// Skeleton Loading Component export const TransactionListSkeleton: React.FC = () => ( <div className="space-y-4"> {[...Array(3)].map((_, index) => ( <div key={index} className="flex items-center justify-between py-3"> <ShimmerText className="w-32" /> <ShimmerText className="w-24" /> </div> ))} </div> ); // Loading State Management in Hooks const [isLoading, setIsLoading] = useState(true); const loadWallet = useCallback(async (id: string) => { setIsLoading(true); try { const walletData = await walletService.getWallet(id); setWallet(walletData); } finally { setIsLoading(false); } }, []);
-
Memory Management
- Cleanup of event listeners
- API call abortion on unmount
- Proper disposal of subscriptions
- Clear interval/timeout cleanup
// API Call Cleanup export const useApiPagination = <T>({ fetchFn, limit }: Props<T>) => { const abortControllerRef = useRef<AbortController | null>(null); const loadPage = useCallback(async (pageNum: number) => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; // ... fetch logic }, [fetchFn, limit]); useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); };
-
Form Optimizations
- Debounced form submissions
- Field-level validation
- Dynamic form fields
- Type-safe form handling
// Debounced Form Submission export const useDebouncedSubmit = <T extends FieldValues>( handleSubmit: UseFormHandleSubmit<T>, onSubmit: (data: T) => Promise<void>, wait: number = DEBOUNCE_TIME ) => { const debouncedSubmit = useRef( debounce(handleSubmit(onSubmit), wait) ).current; useEffect(() => { return () => debouncedSubmit.cancel(); }, [debouncedSubmit]); return debouncedSubmit; };
-
API Integration
- Response transformers
- Error handling
// Response Validation and Error Handling private async validateResponse<T>(response: T, schema: z.ZodSchema): Promise<T> { try { return schema.parse(response); } catch (error) { if (error instanceof z.ZodError) { const validationError = new ValidationError('Response validation failed', error); handleApiError(validationError); throw validationError; } throw error; } }
-
Security
- Input sanitization
-
State Management
- Context optimization
- Props drilling prevention
- State colocation
- Atomic updates
// Wallet State Management with Context export const useWalletState = () => { const [walletId, setWalletIdState] = useState<string | null>(() => localStorage.getItem(WALLET_STORAGE_KEY) ); const [wallet, setWallet] = useState<Wallet | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); const setWalletId = useCallback(async (id: string) => { setWalletIdState(id); localStorage.setItem(WALLET_STORAGE_KEY, id); const success = await loadWallet(id); if (!success) { clearWallet(); } }, [loadWallet]); return { state: { walletId, wallet, isLoading, error }, actions: { setWalletId, clearWallet } }; };
-
UI/UX Considerations
- Responsive design
- Loading feedback
- Error states
- Success confirmations