"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";