"use client"; import { Component, createElement, useContext, useMemo, Suspense, type ReactNode, } from "react"; import { OutletContext, type OutletContextValue } from "./outlet-context.js"; import { type ClientErrorBoundaryFallbackProps, type ErrorInfo, type LoaderDefinition, type ResolvedSegment, } from "./types"; import { RouteContentWrapper, LoaderBoundary, } from "./route-content-wrapper.js"; import { OutletProvider } from "./outlet-provider.js"; import { MountContextProvider } from "./browser/react/mount-context.js"; import { getMemoizedContentPromise } from "./segment-content-promise.js"; /** * Render the content for a named parallel/intercept slot segment. * * Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a * segment from context.parallel by slot name and then render it through the * same layout/loader/mountPath wrapping pipeline. */ function renderSlotContent(segment: ResolvedSegment | null): ReactNode { if (!segment) return null; const content: ReactNode = segment.loading || segment.component instanceof Promise ? ( ) : ( (segment.component ?? null) ); const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds); const loaderWrapped = hasOwnLoaders ? ( {content} ) : null; let result: ReactNode; if (segment.layout) { // Layout renders immediately; if loaders exist, the LoaderBoundary becomes // the outlet content so layout's suspends until loaders resolve. result = ( {segment.layout} ); } else if (hasOwnLoaders) { // No layout but has loaders — wrap content with LoaderBoundary for useLoader context. // Common for intercept routes that use useLoader without a custom layout. result = loaderWrapped; } else { result = content; } if (segment.mountPath) { return ( {result} ); } return result; } function useSlotSegment( context: OutletContextValue | null, name: `@${string}` | undefined, ): ResolvedSegment | null { return useMemo(() => { if (!name || !context?.parallel) return null; return context.parallel.find((seg) => seg.slot === name) ?? null; }, [context, name]); } /** * Outlet component - renders child content in layouts * * If the current segment defines a loading component, the outlet content * is wrapped in Suspense with the loading component as fallback. * This means during navigation/streaming, React's Suspense will automatically * show the loading skeleton until the content is ready. * * When a name prop is provided (e.g., "@modal"), renders content from * the parallel segment with that slot name instead of the default content. * This is used for parallel routes and intercepting routes. * * @param name - Optional slot name for parallel/intercept content (must start with @) * * @example * ```tsx * function BlogLayout() { * return ( *
*

Blog

* *
* ); * } * * // With named slot for modal/parallel content: * function KanbanLayout() { * return ( *
* * * *
* ); * } * ``` */ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode { const context = useContext(OutletContext); const namedSegment = useSlotSegment(context, name); if (name) { return renderSlotContent(namedSegment); } // Default: render child content const content = context?.content ?? null; // If this segment defines a loading component, wrap outlet content with Suspense // The loading component becomes the Suspense fallback, shown during streaming/navigation if (context?.loading) { return {content}; } return content; } /** * ParallelOutlet component - renders content for a named parallel slot * * If the parallel segment defines a loading component, the content * is wrapped in Suspense with the loading component as fallback. * This enables streaming and navigation loading states for parallels. * * @param name - The slot name (must start with @, e.g., "@modal", "@sidebar") * * @example * ```tsx * function DashboardLayout() { * return ( *
*

Dashboard

* * *
* ); * } * ``` */ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode { const context = useContext(OutletContext); const segment = useSlotSegment(context, name); return renderSlotContent(segment); } // OutletProvider is defined in outlet-provider.tsx to break a circular // dependency between client.tsx and route-content-wrapper.tsx. // Imported at the top of this file for local use in Outlet/ParallelOutlet, // and re-exported here for backwards compatibility. export { OutletProvider }; /** * Hook to access outlet content programmatically * * Alternative to using component. Useful when you need * direct access to the outlet content in your logic. * * @example * ```tsx * function BlogLayout() { * const outlet = useOutlet(); * return

Blog

{outlet}
; * } * ``` */ export function useOutlet(): ReactNode { const context = useContext(OutletContext); return context?.content ?? null; } // Loader hooks - re-exported from dedicated file export { useLoader, useFetchLoader, useRefreshLoaders, type LoadFunction, type UseLoaderResult, type UseFetchLoaderResult, type UseLoaderOptions, } from "./use-loader.js"; /** * Props for the ErrorBoundary component */ export interface ErrorBoundaryProps { /** Fallback UI to show when an error is caught */ fallback: | ReactNode | ((props: ClientErrorBoundaryFallbackProps) => ReactNode); /** Children to render */ children: ReactNode; /** Optional callback when an error is caught */ onError?: (error: Error, errorInfo: React.ErrorInfo) => void; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } /** * Client-side ErrorBoundary component * * Catches JavaScript errors in child components during rendering, * in lifecycle methods, and in constructors of the whole tree below them. * Displays a fallback UI instead of the component tree that crashed. * * Use this to wrap client components that might throw during hydration * or user interaction. For server-side errors (middleware, loaders, handlers), * use the errorBoundary() helper in route definitions instead. * * @example * ```tsx * "use client"; * import { ErrorBoundary } from "rsc-router/client"; * * function MyComponent() { * return ( * Something went wrong}> * * * ); * } * * // Or with a function fallback for more control: * function MyComponent() { * return ( * ( *
*

Error: {error.message}

* *
* )} * > * *
* ); * } * ``` */ export class ErrorBoundary extends Component< ErrorBoundaryProps, ErrorBoundaryState > { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { console.error("[ErrorBoundary] Error caught:", error, errorInfo); this.props.onError?.(error, errorInfo); } reset = (): void => { this.setState({ hasError: false, error: null }); }; render(): ReactNode { if (this.state.hasError && this.state.error) { const { fallback } = this.props; // Create error info for the fallback const errorInfo: ErrorInfo = { message: this.state.error.message, name: this.state.error.name, stack: this.state.error.stack, cause: this.state.error.cause, segmentId: "client", segmentType: "route", }; // Render fallback - use createElement so hooks work in function fallbacks if (typeof fallback === "function") { return createElement(fallback, { error: errorInfo, reset: this.reset }); } return fallback; } return this.props.children; } } // ============================================================================ // Re-exports from browser/react for convenience // These are the most commonly used client-side navigation utilities // ============================================================================ // Navigation hooks export { useNavigation } from "./browser/react/use-navigation.js"; export { useRouter } from "./browser/react/use-router.js"; export { usePathname } from "./browser/react/use-pathname.js"; export { useSearchParams } from "./browser/react/use-search-params.js"; export { useParams } from "./browser/react/use-params.js"; export type { RouterInstance, RouterNavigateOptions, ReadonlyURLSearchParams, } from "./browser/types.js"; // Action state tracking hook export { useAction, type ServerActionFunction, } from "./browser/react/use-action.js"; // Segments state hook export { useSegments, type SegmentsState, } from "./browser/react/use-segments.js"; // Client cache controls hook export { useClientCache, type ClientCacheControls, } from "./browser/react/use-client-cache.js"; // Provider export { NavigationProvider, type NavigationProviderProps, } from "./browser/react/NavigationProvider.js"; // Link component export { Link, type LinkProps, type PrefetchStrategy, type StateOrGetter, } from "./browser/react/Link.js"; // Link status hook export { useLinkStatus, type LinkStatus, } from "./browser/react/use-link-status.js"; // Scroll restoration export { ScrollRestoration, useScrollRestoration, type ScrollRestorationProps, } from "./browser/react/ScrollRestoration.js"; // Handle data hook (client-side only — createHandle/isHandle are server APIs from the root export) export { type Handle } from "./handle.js"; export { useHandle } from "./browser/react/use-handle.js"; // Built-in handles export { Meta } from "./handles/meta.js"; export { MetaTags } from "./handles/MetaTags.js"; export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js"; export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js"; // Location state - type-safe navigation state export { createLocationState, useLocationState, type LocationStateDefinition, type LocationStateEntry, type LocationStateOptions, } from "./browser/react/location-state.js"; // Type-safe href for client-side path validation. The path and response types // are ambient as `Rango.Path` / `Rango.PathResponse` (declared in // href-client.ts) — no import needed. export { href, type PatternToPath } from "./href-client.js"; // Response envelope types for consuming JSON response routes export type { ResponseEnvelope, ResponseError } from "./urls.js"; /** * Type guard for checking if a response envelope contains an error. * * @example * ```typescript * const result: ResponseEnvelope = await fetch(url).then(r => r.json()); * if (isResponseError(result)) { * console.log(result.error.message, result.error.code); * return; * } * result.data // fully typed as Product * ``` */ export function isResponseError( result: import("./urls.js").ResponseEnvelope, ): result is import("./urls.js").ResponseEnvelope & { error: import("./urls.js").ResponseError; } { return result.error !== undefined; } // Mount context for include() scoped components export { useMount } from "./browser/react/use-mount.js"; export { MountContext } from "./browser/react/mount-context.js"; // Mount-aware href hook - auto-prefixes paths with include() mount export { useHref } from "./browser/react/use-href.js"; // Mount-aware reverse hook - resolves dot-prefixed names against an imported // generated routes map (from a urls() module's .gen.ts). export { useReverse } from "./browser/react/use-reverse.js"; // Type-safe scoped reverse function for scopedReverse() export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js"; // Loader definition type - for typing loader props in client components export type { LoaderDefinition } from "./types.js";