diff --git a/.Jules/changelog.md b/.Jules/changelog.md index d438210..16a7b0c 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,14 @@ ## [Unreleased] ### Added +- **Error Boundary System:** Implemented a global React Error Boundary to catch render errors gracefully. + - **Features:** + - Dual-theme support (Glassmorphism & Neobrutalism) for the error fallback UI. + - "Retry" button to reset error state and re-render. + - "Home" button to navigate back to safety. + - Captures errors in `AppRoutes` and displays a user-friendly message instead of a white screen. + - **Technical:** Created `web/components/ErrorBoundary.tsx` using a hybrid Class+Functional approach to support Hooks in the fallback UI. Integrated into `web/App.tsx`. + - Inline form validation in Auth page with real-time feedback and proper ARIA accessibility support (`aria-invalid`, `aria-describedby`, `role="alert"`). - Dashboard skeleton loading state (`DashboardSkeleton`) to improve perceived performance during data fetch. - Comprehensive `EmptyState` component for Groups and Friends pages to better guide new users. diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index d69c659..ad48a44 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -87,6 +87,33 @@ colors: { ## Component Patterns +### Error Boundary Pattern + +**Date:** 2026-01-14 +**Context:** Implementing global error handling + +React Error Boundaries must be class components to use `componentDidCatch`. However, to use hooks (like `useTheme` or `useNavigate`), you should split the fallback UI into a separate functional component. + +```tsx +// 1. Functional Fallback (uses hooks) +const ErrorFallback = ({ error, resetErrorBoundary }) => { + const { style } = useTheme(); + return
...
; +} + +// 2. Class Boundary (handles logic) +class ErrorBoundary extends Component { + render() { + if (this.state.hasError) { + return ; + } + return this.props.children; + } +} +``` + +**Testing Tip:** React Error Boundaries do **not** catch errors in event handlers. To verify them, you must throw inside the `render` method (e.g., `if (shouldThrow) throw new Error()`). + ### Button Component Variants **Date:** 2026-01-01 @@ -459,6 +486,30 @@ _Document errors and their solutions here as you encounter them._ ## Recent Implementation Reviews +### ✅ Successful PR Pattern: Error Boundary (#240) + +**Date:** 2026-01-14 +**Context:** Implementing global error handling + +**What was implemented:** +1. Created `ErrorBoundary.tsx` with class component + functional fallback. +2. Wrapped `AppRoutes` in `App.tsx` with `ErrorBoundary`. +3. Styled fallback UI for both Neobrutalism and Glassmorphism. +4. Added "Retry" and "Go Home" options. + +**Why it succeeded:** +- ✅ Complete system (Component + Integration + Styling). +- ✅ Solved "React Hooks inside Class Component" by splitting logic. +- ✅ Verified using a temporary render-phase throw (not event handler). +- ✅ Maintained dual-theme support. + +**Key learnings:** +- Error Boundaries only catch errors in Render/Lifecycle, NOT event handlers. +- Verification requires triggering an error during render (e.g., conditional throw). +- Must wrap Router if using `useNavigate` or `Link` in fallback. + +--- + ### ✅ Successful PR Pattern: Toast Notification System (#227) **Date:** 2026-01-13 diff --git a/.Jules/todo.md b/.Jules/todo.md index 894e27f..4539a8a 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -34,12 +34,12 @@ - Impact: Guides new users, makes app feel polished - Size: ~70 lines -- [ ] **[ux]** Error boundary with retry for API failures - - Files: Create `web/components/ErrorBoundary.tsx`, wrap app +- [x] **[ux]** Error boundary with retry for API failures + - Completed: 2026-01-14 + - Files: Created `web/components/ErrorBoundary.tsx`, wrapped app - Context: Catch errors gracefully with retry button - Impact: App doesn't crash, users can recover - - Size: ~60 lines - - Added: 2026-01-01 + - Size: ~80 lines ### Mobile @@ -154,5 +154,9 @@ - Completed: 2026-01-11 - Files modified: `web/pages/Auth.tsx` - Impact: Users know immediately if input is valid via inline error messages and red borders. +- [x] **[ux]** Error boundary with retry for API failures + - Completed: 2026-01-14 + - Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx` + - Impact: App doesn't crash, users can recover _No tasks completed yet. Move tasks here after completion._ diff --git a/web/App.tsx b/web/App.tsx index 1461005..0a6d4c6 100644 --- a/web/App.tsx +++ b/web/App.tsx @@ -6,6 +6,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'; import { ThemeProvider } from './contexts/ThemeContext'; import { ToastProvider } from './contexts/ToastContext'; import { ToastContainer } from './components/ui/Toast'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { Auth } from './pages/Auth'; import { Dashboard } from './pages/Dashboard'; import { Friends } from './pages/Friends'; @@ -51,7 +52,9 @@ const App = () => { - + + + diff --git a/web/components/ErrorBoundary.tsx b/web/components/ErrorBoundary.tsx new file mode 100644 index 0000000..b9be578 --- /dev/null +++ b/web/components/ErrorBoundary.tsx @@ -0,0 +1,86 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; +import { Button } from './ui/Button'; +import { Card } from './ui/Card'; +import { useTheme } from '../contexts/ThemeContext'; +import { THEMES } from '../constants'; +import { useNavigate } from 'react-router-dom'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +// Functional component to access hooks +const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error | null, resetErrorBoundary: () => void }) => { + const { style } = useTheme(); + const navigate = useNavigate(); + + const isNeo = style === THEMES.NEOBRUTALISM; + + const handleHome = () => { + resetErrorBoundary(); + navigate('/'); + }; + + return ( +
+ +
+
+
+ +
+

Something went wrong

+

+ {error?.message || "An unexpected error occurred."} +

+
+ +
+ + +
+
+
+
+ ); +}; + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public resetErrorBoundary = () => { + this.setState({ hasError: false, error: null }); + }; + + public render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +}