Remix: Fetchers, Sessions & HTTP Caching
useFetcher: Non-Navigational Mutations
import { useFetcher } from "@remix-run/react";
// A "like" button that submits without changing the URL or triggering
// a full navigation -- unlike <Form>, which does navigate
function LikeButton({ postId, initiallyLiked }: { postId: string; initiallyLiked: boolean }) {
const fetcher = useFetcher();
// Optimistic UI: while the submission is in flight, fetcher.formData
// holds the pending values -- render the assumed result immediately
const isLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: initiallyLiked;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<input type="hidden" name="liked" value={String(!isLiked)} />
<button type="submit">{isLiked ? "♥" : "♡"}</button>
</fetcher.Form>
);
}
// useNavigation() reflects pending navigation/submission state --
// commonly used for a loading indicator or disabling a submit button
import { useNavigation } from "@remix-run/react";
function SubmitButton() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return <button disabled={isSubmitting}>{isSubmitting ? "Saving..." : "Save"}</button>;
}Sessions & Auth
// app/sessions.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
secrets: [process.env.SESSION_SECRET!],
sameSite: "lax",
},
});
export async function requireUserId(request: Request) {
const session = await sessionStorage.getSession(request.headers.get("Cookie"));
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
return userId;
}
// In a protected route's loader
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
return json(await getDashboardData(userId));
}HTTP Caching via headers()
// Remix leans on real HTTP semantics for caching, not a custom API
export function headers() {
return {
"Cache-Control": "public, max-age=300, s-maxage=3600",
};
}
// Or set headers directly on the loader's Response
export async function loader() {
const data = await getPublicStats();
return json(data, {
headers: { "Cache-Control": "public, max-age=60" },
});
}
// A CDN/browser in front of the app can cache using these standard
// headers -- no Remix-specific cache invalidation API to learn.Breadcrumbs via handle + useMatches
// A layout route can't normally see its active child route's specifics --
// handle + useMatches is the standard way to bridge that
// app/routes/projects.$projectId.tsx
export const handle = {
breadcrumb: (data: { name: string }) => data.name,
};
// app/root.tsx (or any ancestor layout)
import { useMatches } from "@remix-run/react";
function Breadcrumbs() {
const matches = useMatches();
const crumbs = matches
.filter((match) => match.handle?.breadcrumb)
.map((match) => match.handle.breadcrumb(match.data));
return <nav>{crumbs.join(" / ")}</nav>;
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free