React DOM bindings for Remix web framework providing components, hooks, and utilities for full-stack React applications
—
Hooks for accessing server-loaded data, handling form submissions, and managing client-server communication in Remix applications.
Access data loaded by the current route's loader function.
/**
* Returns data loaded by the current route's loader function
* Data is automatically serialized and type-safe with proper TypeScript inference
* @returns The data returned from the route's loader function
*/
function useLoaderData<T = unknown>(): SerializeFrom<T>;Usage Examples:
import { useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
// Loader function
export async function loader({ params }: LoaderFunctionArgs) {
const user = await getUser(params.userId);
const posts = await getUserPosts(params.userId);
return {
user,
posts,
lastLogin: new Date(),
};
}
// Component using loader data
export default function UserProfile() {
const { user, posts, lastLogin } = useLoaderData<typeof loader>();
return (
<div>
<h1>{user.name}</h1>
<p>Last login: {new Date(lastLogin).toLocaleDateString()}</p>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}Access data returned by the current route's action function after form submission.
/**
* Returns data from the current route's action function
* Only available after a form submission, returns undefined otherwise
* @returns The data returned from the route's action function or undefined
*/
function useActionData<T = unknown>(): SerializeFrom<T> | undefined;Usage Examples:
import { useActionData, Form } from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/node";
// Action function
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
try {
await subscribeToNewsletter(email);
return { success: true, message: "Successfully subscribed!" };
} catch (error) {
return {
success: false,
message: "Failed to subscribe",
errors: { email: "Invalid email address" }
};
}
}
// Component using action data
export default function Newsletter() {
const actionData = useActionData<typeof action>();
return (
<div>
<Form method="post">
<input
type="email"
name="email"
required
className={actionData?.errors?.email ? "error" : ""}
/>
<button type="submit">Subscribe</button>
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</Form>
{actionData?.message && (
<div className={actionData.success ? "success" : "error"}>
{actionData.message}
</div>
)}
</div>
);
}Access loader data from any route in the current route hierarchy by route ID.
/**
* Returns loader data from a specific route by route ID
* Useful for accessing parent route data or shared data across route boundaries
* @param routeId - The ID of the route whose loader data to access
* @returns The loader data from the specified route or undefined if not found
*/
function useRouteLoaderData<T = unknown>(routeId: string): SerializeFrom<T> | undefined;Usage Examples:
import { useRouteLoaderData } from "@remix-run/react";
// In app/root.tsx
export async function loader() {
return {
user: await getCurrentUser(),
environment: process.env.NODE_ENV,
};
}
// In any child route component
export default function ChildRoute() {
// Access root loader data using route ID
const rootData = useRouteLoaderData<typeof loader>("root");
if (!rootData?.user) {
return <div>Please log in to continue</div>;
}
return (
<div>
<p>Welcome, {rootData.user.name}!</p>
<p>Environment: {rootData.environment}</p>
</div>
);
}
// In a nested layout accessing parent data
export default function DashboardLayout() {
// Access dashboard loader data from child routes
const dashboardData = useRouteLoaderData("routes/dashboard");
return (
<div>
<nav>Dashboard Navigation</nav>
{dashboardData && <UserStats stats={dashboardData.stats} />}
<Outlet />
</div>
);
}Create a fetcher for loading data or submitting forms without causing navigation.
/**
* Creates a fetcher for data loading and form submission without navigation
* Provides a way to interact with loaders and actions programmatically
* @param opts - Optional fetcher configuration
* @returns Fetcher instance with form components and submission methods
*/
function useFetcher<T = unknown>(opts?: FetcherOptions): FetcherWithComponents<T>;
interface FetcherOptions {
/** Key to identify this fetcher instance for reuse */
key?: string;
}
interface FetcherWithComponents<T = unknown> {
/** Current state of the fetcher */
state: "idle" | "loading" | "submitting";
/** Data returned from the last successful fetch */
data: SerializeFrom<T> | undefined;
/** Form data being submitted */
formData: FormData | undefined;
/** JSON data being submitted */
json: any;
/** Text data being submitted */
text: string | undefined;
/** HTTP method of the current/last submission */
formMethod: string | undefined;
/** Action URL of the current/last submission */
formAction: string | undefined;
/** Form component for this fetcher */
Form: React.ComponentType<FormProps>;
/** Submit function for programmatic submissions */
submit: SubmitFunction;
/** Load function for programmatic data loading */
load: (href: string) => void;
}Usage Examples:
import { useFetcher } from "@remix-run/react";
// Simple data fetching
function UserSearch() {
const fetcher = useFetcher();
const handleSearch = (query: string) => {
fetcher.load(`/api/users/search?q=${encodeURIComponent(query)}`);
};
return (
<div>
<input
type="search"
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search users..."
/>
{fetcher.state === "loading" && <div>Searching...</div>}
{fetcher.data && (
<ul>
{fetcher.data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
// Form submission without navigation
function QuickAddForm() {
const fetcher = useFetcher();
const isAdding = fetcher.state === "submitting";
const isSuccess = fetcher.data?.success;
return (
<fetcher.Form method="post" action="/api/quick-add">
<input name="title" placeholder="Quick add..." required />
<button type="submit" disabled={isAdding}>
{isAdding ? "Adding..." : "Add"}
</button>
{isSuccess && <div>Added successfully!</div>}
</fetcher.Form>
);
}
// Programmatic form submission
function LikeButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
const handleLike = () => {
fetcher.submit(
{ postId, action: "like" },
{ method: "post", action: "/api/posts/like" }
);
};
return (
<button
onClick={handleLike}
disabled={fetcher.state === "submitting"}
>
{fetcher.state === "submitting" ? "Liking..." : "Like"}
</button>
);
}Access information about all matched routes in the current route hierarchy.
/**
* Returns information about all matched routes in the current hierarchy
* Includes route IDs, pathnames, data, and route-specific metadata
* @returns Array of matched route information
*/
function useMatches(): UIMatch[];
interface UIMatch {
/** Unique identifier for the route */
id: string;
/** Path pattern that matched */
pathname: string;
/** Route parameters */
params: Params;
/** Data from the route's loader */
data: unknown;
/** Custom route handle data */
handle: RouteHandle | undefined;
}Usage Examples:
import { useMatches } from "@remix-run/react";
// Breadcrumb navigation
function Breadcrumbs() {
const matches = useMatches();
const breadcrumbs = matches.filter(match =>
match.handle?.breadcrumb
).map(match => ({
label: match.handle.breadcrumb(match),
pathname: match.pathname,
}));
return (
<nav>
{breadcrumbs.map((crumb, index) => (
<span key={crumb.pathname}>
{index > 0 && " > "}
<Link to={crumb.pathname}>{crumb.label}</Link>
</span>
))}
</nav>
);
}
// Page title based on route hierarchy
function PageTitle() {
const matches = useMatches();
const titles = matches
.map(match => match.handle?.title?.(match))
.filter(Boolean);
const pageTitle = titles.join(" | ");
useEffect(() => {
document.title = pageTitle;
}, [pageTitle]);
return null;
}/**
* Represents the serialized form of data returned from loaders/actions
* Handles Date objects, nested objects, and other non-JSON types
*/
type SerializeFrom<T> = T extends (...args: any[]) => infer R
? SerializeFrom<R>
: T extends Date
? string
: T extends object
? { [K in keyof T]: SerializeFrom<T[K]> }
: T;interface RouteHandle {
/** Function to generate breadcrumb label */
breadcrumb?: (match: UIMatch) => React.ReactNode;
/** Function to generate page title */
title?: (match: UIMatch) => string;
/** Custom metadata for the route */
[key: string]: any;
}interface FormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
method?: "get" | "post" | "put" | "patch" | "delete";
action?: string;
encType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain";
replace?: boolean;
preventScrollReset?: boolean;
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;
}Install with Tessl CLI
npx tessl i tessl/npm-remix-run--react