import React from 'react'; import { LoaderFunctionArgs } from 'react-router'; /** * Props for RouteErrorBoundary component * * For consistent user experience, custom error components should ideally match * the styling and behavior of your server-side error pages (configured via * `get500ErrorPage` in ServeSSROptions for SSR applications). */ interface RouteErrorBoundaryProps { /** Custom component to render for 404 Not Found errors */ NotFoundComponent?: React.ComponentType<{ error?: unknown; }>; /** Custom component to render for application errors */ ApplicationErrorComponent?: React.ComponentType<{ error: unknown; }>; } /** * Customizable route error boundary that handles 404s and application errors * * Consider matching server-side error pages (get500ErrorPage in ServeSSROptions) for consistent UX. * * @param NotFoundComponent - Custom component for 404 errors (receives error prop) * @param ApplicationErrorComponent - Custom component for application errors (receives error prop) */ declare function RouteErrorBoundary({ NotFoundComponent, ApplicationErrorComponent, }?: RouteErrorBoundaryProps): React.JSX.Element; interface PageMetadata { title: string; description: string; keywords?: string; canonical?: string; og?: { title?: string; description?: string; image?: string; }; } /** * Base meta structure with required page metadata */ interface BaseMeta { page?: PageMetadata; } interface ErrorDetails { [key: string]: unknown; } /** * Error details value - supports both object and array formats * * @example Object format (structured errors) * { field: 'email', reason: 'invalid format', code: 'VALIDATION_ERROR' } * * @example Array format (multiple validation errors with type field) * [ * { field: 'email', type: 'invalid_email', message: 'Must be a valid email address' }, * { field: 'password', type: 'invalid_length', message: 'Must be at least 8 characters long' } * ] * * @example Array format (error trace) * ['Step 1 failed', 'Rollback initiated', 'Cleanup completed'] */ type ErrorDetailsValue = ErrorDetails | unknown[]; /** * Error object structure for API error responses */ interface ErrorObject { code: string; message: string; details?: ErrorDetailsValue; } interface RedirectInfo { target: string; permanent: boolean; preserve_query?: boolean; } /** * API Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) * * @example * // Basic usage (no extra meta) * type BasicResponse = APISuccessResponse; * * @example * // With required extra meta fields * interface CustomMeta extends BaseMeta { * pagination: { page: number; total: number }; * cache: { expires: string }; * } * type PaginatedResponse = APISuccessResponse; */ interface APISuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: T; meta: M; error?: null; } /** * API Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface APIErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: null; meta: M; error: ErrorObject; } /** * API response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type APIResponseEnvelope = APISuccessResponse | APIErrorResponse; /** * Page Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) */ interface PageSuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: T; meta: M; error?: null; ssr_request_context?: Record; } /** * Page Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error: ErrorObject; ssr_request_context?: Record; } /** * Page Redirect Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageRedirectResponse { status: 'redirect'; status_code: 200; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error?: null; redirect: RedirectInfo; ssr_request_context?: Record; } /** * Page response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type PageResponseEnvelope = PageSuccessResponse | PageErrorResponse | PageRedirectResponse; /** * Detects envelope-based errors from any route in the current hierarchy. * * Typically used by a parent layout component to detect errors from child routes. * Uses useMatches() instead of useLoaderData() because useLoaderData() only returns * the current route's data, but we need to inspect data from child routes. */ declare function useDataLoaderEnvelopeError(): { hasError: boolean; is404: boolean; errorResponse: PageErrorResponse | null; }; /** * Base error definition with title and description */ interface BaseErrorDefinition { title: string; description: string; } /** * Full error definition with title, description, message, and code */ interface FullErrorDefinition extends BaseErrorDefinition { message: string; code: string; } /** * Shared interface for all error defaults used in both DEFAULT_ERROR_DEFAULTS const and PageDataLoaderConfig interface */ interface ErrorDefaults { notFound: FullErrorDefinition; internalError: FullErrorDefinition; authRequired: BaseErrorDefinition; accessDenied: FullErrorDefinition; genericError: FullErrorDefinition; invalidResponse: FullErrorDefinition; invalidRedirect: FullErrorDefinition; redirectNotFollowed: FullErrorDefinition; unsafeRedirect: FullErrorDefinition; } /** * Custom status code handler function type * * @param statusCode - The HTTP status code from the API response * @param responseData - The parsed JSON response data from the API (if valid JSON) * @param config - The page data loader configuration * @returns A PageResponseEnvelope or null/undefined to fall back to default handling * * For redirects, use the API envelope redirect pattern with status: "redirect". * Note: You don't need to include __ssOnly data - the loader framework automatically * decorates your envelope with SSR-only data (cookies, etc.) when needed. */ type CustomStatusCodeHandler = (statusCode: number, responseData: unknown, config: PageDataLoaderConfig, isDevelopment: boolean) => PageResponseEnvelope | null | undefined; /** * Shared configuration used by both HTTP-backed and local page data loaders. * * Local loaders do not use `APIBaseURL`, but they can still produce * auth/redirect-style responses and page envelopes, so shared redirect/auth * settings such as `loginURL` and `returnToParam` live here. */ interface BasePageDataLoaderConfig { /** Default error constants for common scenarios */ errorDefaults: ErrorDefaults; /** User-facing messages for timeout/connection-style failures */ connectionErrorMessages?: { /** Message shown for server-side timeout/connection-style failures */ server?: string; /** Message shown for client-side timeout/connection-style failures */ client?: string; }; /** Login URL for authentication redirects (e.g., "/login") */ loginURL: string; /** Query parameter name for return URL in login redirects (default: "return_to") */ returnToParam?: string; /** * Timeout in milliseconds for API requests * * Defaults to 10000ms (10 seconds). Set to 0 to disable timeout. * Uses AbortController to cancel requests that exceed the timeout. * * @default 10000 */ timeoutMS?: number; /** * Function to generate fallback request IDs when none is provided in error responses * Called when creating error responses that don't have a request_id from the API * @param context - The context for the request ID ("error" or "redirect") * @returns A unique identifier string for the request */ generateFallbackRequestID?: (context: 'error' | 'redirect') => string; /** * Optional function to transform/extend the meta object when converting API errors to page errors * * This is called when: * 1. An API endpoint returns an error response (type: "api") * 2. The pageDataLoader needs to convert it to a page response (type: "page") for React Router * 3. Metadata from the original API response needs to be preserved/transformed * * Common use case: API returns user account info, site settings, etc. in meta, * and you want to preserve that data in the converted page error response. * * @param baseMeta - The base page meta (title/description) already populated * @param statusCode - HTTP status code from the original response * @param errorCode - Error code from the API response * @param originalMetadata - Original metadata from API response (account, site_info, etc.) * @returns Extended meta object with app-specific fields added to baseMeta */ transformErrorMeta?: (params: { baseMeta: BaseMeta; statusCode: number; errorCode: string; originalMetadata?: { title?: string; description?: string; [key: string]: unknown; }; }) => BaseMeta | Record; /** * Optional array of allowed origins for redirect safety validation * * Behavior: * - **undefined**: Redirect safety validation is disabled (any redirect target allowed) * - **empty array []**: Only relative paths allowed (all external URLs blocked) * - **array with origins**: Relative paths + specified origins allowed * * Example: * ```typescript * // Allow any redirect (validation disabled) * allowedRedirectOrigins: undefined * * // Only allow relative paths, block all external URLs * allowedRedirectOrigins: [] * * // Allow relative paths + specific origins * allowedRedirectOrigins: [ * "https://myapp.com", * "https://auth.myapp.com" * ] * * // Allow relative paths + SaaS tenant subdomains * allowedRedirectOrigins: [ * "https://*.myapp.com" * ] * ``` * * With specific origins configured, this allows: * - "/dashboard" (relative path - always allowed) * - "https://myapp.com/profile" (allowed origin) * - "https://auth.myapp.com/login" (allowed origin) * - "https://tenant.myapp.com/dashboard" (wildcard origin pattern) * * But blocks: * - "https://evil.com/phishing" (not in allowed origins) * - "javascript:alert('xss')" (not a valid origin) */ allowedRedirectOrigins?: string[]; } /** * Configuration for local-only page data loaders (no framework HTTP fetch). * Includes only the fields actually used by the local data loader path. */ type LocalPageDataLoaderConfig = BasePageDataLoaderConfig; /** * Configuration object interface for the HTTP-backed page data loader system. */ interface PageDataLoaderConfig extends BasePageDataLoaderConfig { /** Base URL for the API server (e.g., "http://localhost:3001" or "https://api.example.com") */ APIBaseURL: string; /** * Page data endpoint path (e.g., "/api/v1/page_data" or "/api/page_data") * * This path will be appended to the APIBaseURL to form the complete endpoint URL. * The pageType will be appended to this path: `{APIBaseURL}{pageDataEndpoint}/{pageType}` * * @default "/api/v1/page_data" */ pageDataEndpoint?: string; /** * Optional map of custom status code handlers * * Allows you to provide custom handling logic for specific HTTP status codes. * Use '*' as a key for a wildcard handler that catches all status codes (checked after specific handlers). * If a handler returns null or undefined, the default handling logic will be used. * * You can manually construct PageResponseEnvelope objects. For redirects, use the API envelope * redirect pattern with status: "redirect". The APIResponseHelpers are designed for server-side * API handlers and require a FastifyRequest object, so they're not suitable for use in data loaders. * * Example: * ```typescript * statusCodeHandlers: { * // Wildcard handler for all status codes (checked after specific handlers) * '*': (statusCode, responseData, config, isDevelopment) => { * // Log all errors in development * if (isDevelopment) { * console.error(`Unhandled status code ${statusCode}:`, responseData); * } * * // Return null to fall back to default handling * return null; * }, * // Custom handling for 418 I'm a teapot (number key) * 418: (statusCode, responseData, config) => { * return { * status: 'error', * status_code: 418, * request_id: responseData?.request_id || `teapot_${Date.now()}`, * type: 'page', * data: null, * meta: { page: { title: 'I\'m a teapot', description: 'Cannot brew coffee' } }, * error: { code: 'teapot_error', message: 'I\'m a teapot! Cannot brew coffee.' } * }; * }, * // Override default 404 handling (string key also works) * "404": (statusCode, responseData, config) => { * // Custom 404 logic here... * // Return null to fall back to default 404 handling * return null; * }, * // Handle payment required with API envelope redirect * 402: (statusCode, responseData, config) => { * // Use API envelope redirect pattern (recommended) * return { * status: 'redirect', * status_code: 200, * request_id: responseData?.request_id || `redirect_${Date.now()}`, * type: 'page', * data: null, * meta: { page: { title: 'Payment Required', description: 'Redirecting to payment page' } }, * error: null, * redirect: { * target: '/payment-required', * permanent: false, * preserve_query: false * } * }; * } * } * ``` */ statusCodeHandlers?: Record; } type LocalPageDataLoaderConfigOverrides = Partial; type PageDataLoaderConfigOverrides = Partial>; interface LocalPageHandlerParams { /** Logical page type - set to 'local' by the framework */ pageType: 'local'; /** Origin indicator for debugging */ invocationOrigin: 'local'; /** Route params (from React Router) */ routeParams: Record; /** Query params (from React Router) */ queryParams: Record; /** Request path (from React Router) */ requestPath: string; /** Original URL (from React Router) */ originalURL: string; } type LocalPageHandler = (params: LocalPageHandlerParams) => Promise | APIResponseEnvelope> | PageResponseEnvelope | APIResponseEnvelope; /** * Page Data Loader System * ----------------- * * This is a centralized page data loader system that handles all route data fetching for the application. * Instead of having multiple specialized loaders for separate pages, we've consolidated all page data fetching * into this single data loader that communicates with our API server. * * How it works: * 1. Each route uses createPageDataLoader(config, pageType) to create a loader for that route * 2. The pageDataLoader makes a POST request to {APIBaseURL}{pageDataEndpoint}/{pageType} with route params and query params * (default endpoint: /api/v1/page_data/{pageType}, configurable via pageDataEndpoint option) * 3. The API server handles the request and returns data in a standardized response format * 4. The loader processes the response, handling errors, redirects, and authentication * * Benefits: * - Consistent error handling and response processing * - Centralized authentication flow * - Simplified route definitions * - Easier maintenance with a single loader implementation * * This approach eliminates the need for multiple loader files and consolidates all * data fetching logic in one place, making it easier to maintain and extend. * * Usage Example: * ```typescript * import { createPageDataLoader, createDefaultPageDataLoaderConfig } from './pageDataLoader'; * * // Create a configuration (typically done once in your app setup) * const config = createDefaultPageDataLoaderConfig('http://localhost:3001'); * * // Or create a custom configuration with your own titles/branding * const customConfig = { * APIBaseURL: 'https://api.myapp.com', * pageDataEndpoint: '/api/v1/page_data', // Custom page data endpoint (default: '/api/v1/page_data') * loginURL: '/auth/login', * returnToParam: 'redirect_to', // Custom query param name for login redirects * timeoutMS: 15000, // Custom timeout in milliseconds (default: 10000) * generateFallbackRequestID: (context) => `myapp_${context}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, * connectionErrorMessages: { * server: 'API service unavailable. Please try again later.', * client: 'Network error. Please check your connection and try again.', * }, * errorDefaults: { * notFound: { * title: 'Page Not Found | MyApp', * description: 'The page you are looking for could not be found.', * code: 'not_found', * message: 'The requested resource was not found.', * }, * internalError: { * title: 'Server Error | MyApp', * description: 'An internal server error occurred.', * code: 'internal_server_error', * message: 'An internal server error occurred.', * }, * authRequired: { * title: 'Login Required | MyApp', * description: 'You must be logged in to access this page.', * }, * accessDenied: { * title: 'Access Denied | MyApp', * description: 'You do not have permission to access this page.', * message: 'Sorry, you don\'t have access to this feature.', * code: 'access_denied', // Standard error code (can be customized) * }, * genericError: { * title: 'Oops! | MyApp', * description: 'Something unexpected happened.', * message: 'Oops! Something went wrong. Please try again.', * code: 'unknown_error', // Standard error code (can be customized) * }, * invalidResponse: { * title: 'Server Error | MyApp', * description: 'The server sent an unexpected response.', * message: 'Sorry, we received an unexpected response from the server.', * code: 'invalid_response', // Standard error code (can be customized) * }, * invalidRedirect: { * title: 'Invalid Redirect | MyApp', * description: 'The server attempted an invalid redirect.', * message: 'Sorry, the redirect information was incomplete.', * code: 'invalid_redirect', // Standard error code (can be customized) * }, * redirectNotFollowed: { * title: 'Redirect Error | MyApp', * description: 'HTTP redirects from the API are not supported.', * message: 'Sorry, the API attempted an unsupported redirect.', * code: 'api_redirect_not_followed', // Standard error code (can be customized) * }, * unsafeRedirect: { * title: 'Unsafe Redirect Blocked | MyApp', * description: 'The redirect target is not allowed for security reasons.', * message: 'Sorry, this redirect is not allowed for security reasons.', * code: 'unsafe_redirect', // Standard error code (can be customized) * }, * }, * // Optional: Configure allowed redirect origins for security * allowedRedirectOrigins: [ * 'https://myapp.com', * 'https://auth.myapp.com' * ], * // Optional: Transform metadata when converting API errors to page errors * transformErrorMeta: ({ baseMeta, originalMetadata }) => ({ * ...baseMeta, // Keep the base page info (title/description) * // Preserve app-specific fields from the original API response * account: originalMetadata?.account, * site_info: originalMetadata?.site_info || { current_year: new Date().getFullYear() }, * user_preferences: originalMetadata?.user_preferences, * }), * // Optional: Custom status code handlers * statusCodeHandlers: { * // Handle payment required with API envelope redirect * 402: (statusCode, responseData, config) => { * return { * status: 'redirect', * status_code: 200, * request_id: responseData?.request_id || `redirect_${Date.now()}`, * type: 'page', * data: null, * meta: { page: { title: 'Payment Required', description: 'Redirecting to payment page' } }, * error: null, * redirect: { target: '/payment-required', permanent: false } * }; * }, * // Custom handling for maintenance mode * 503: (statusCode, responseData, config) => { * // Manually construct a page error response * return { * status: 'error', * status_code: 503, * request_id: responseData?.request_id || `maint_${Date.now()}`, * type: 'page', * data: null, * meta: { * page: { * title: 'Maintenance Mode', * description: 'Service temporarily unavailable for maintenance.' * } * }, * error: { * code: 'maintenance_mode', * message: 'Service temporarily unavailable for maintenance.' * } * }; * }, * // Override default 404 but fall back if needed * 404: (statusCode, responseData, config) => { * if (responseData?.error?.code === 'special_not_found') { * // Handle special case with manual response * return { * status: 'error', * status_code: 404, * request_id: responseData?.request_id || `404_${Date.now()}`, * type: 'page', * data: null, * meta: { page: { title: 'Special Not Found', description: 'Special page not found' } }, * error: { code: 'special_not_found', message: 'Special page not found' } * }; * } * // Fall back to default handling * return null; * } * } * }; * * // Create loaders for each page type * const homeLoader = createPageDataLoader(config, 'home'); * const dashboardLoader = createPageDataLoader(config, 'dashboard'); * const profileLoader = createPageDataLoader(config, 'profile'); * * // Use in React Router * export const routes: RouteObject[] = [ * { path: '/', loader: homeLoader, element: }, * { path: '/dashboard', loader: dashboardLoader, element: }, * { path: '/profile/:id', loader: profileLoader, element: }, * ]; * ``` */ /** * Creates a default configuration object with sensible defaults */ declare function createDefaultPageDataLoaderConfig(APIBaseURL: string, overrides?: PageDataLoaderConfigOverrides): PageDataLoaderConfig; declare function createDefaultLocalPageDataLoaderConfig(overrides?: LocalPageDataLoaderConfigOverrides): LocalPageDataLoaderConfig; declare function createPageDataLoader(config: PageDataLoaderConfig, pageType: string): (args: LoaderFunctionArgs) => Promise; declare function createPageDataLoader(config: LocalPageDataLoaderConfig, handler: LocalPageHandler): (args: LoaderFunctionArgs) => Promise; export { type BaseErrorDefinition, type BasePageDataLoaderConfig, type CustomStatusCodeHandler, type ErrorDefaults, type FullErrorDefinition, type LocalPageDataLoaderConfig, type LocalPageDataLoaderConfigOverrides, type LocalPageHandler, type LocalPageHandlerParams, type PageDataLoaderConfig, type PageDataLoaderConfigOverrides, RouteErrorBoundary, type RouteErrorBoundaryProps, createDefaultLocalPageDataLoaderConfig, createDefaultPageDataLoaderConfig, createPageDataLoader, useDataLoaderEnvelopeError };