React Router v7 APIs
Demonstrating lazy loading, Suspense, and self-contained components
What is Lazy Loading?
Code Splitting
What is Suspense?
React Suspense
Suspense is a React component that handles loading states for lazy-loaded components. It shows a fallback UI while the component is being downloaded.
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>Demo 1: Lazy-Loaded ModalInteractive
Click to Load Modal Component
The Modal component is lazy-loaded. It's not in the initial bundle - it only downloads when you click the button. Watch for the spinner while it loads!
Demo 2: Lazy-Loaded ToastInteractive
Click to Load Toast Component
The Toast component with its inline animations is lazy-loaded. Everything loads together as one self-contained unit.
Demo 3: Skeleton Loading
Better UX with Skeleton Loaders
Instead of showing a spinner, you can show a skeleton that matches the component's shape. This provides better visual continuity.
With Spinner:
With Skeleton:
Code Examples
1. Lazy Import
import { lazy } from "react";
// Component isn't loaded until needed
const Modal = lazy(() =>
import("~/components/ui/Modals")
.then(m => ({ default: m.Modal }))
);2. Wrap with Suspense
<Suspense fallback={<Spinner />}>
<Modal isOpen={showModal}>
Content
</Modal>
</Suspense>3. Self-Contained Components
// When lazy-loaded, everything comes together: // ✅ Component logic // ✅ Inline animations (<style> tag) // ✅ Event handlers // ✅ TypeScript types // ✅ All in one chunk!
Benefits
⚡ Faster Initial Load
📦 Better Code Splitting
🎯 On-Demand Loading
🚀 Cloudflare Edge
💪 Type Safety
🎨 Better UX
Demo 4: Navigation APIsInteractive
useNavigate - Programmatic Navigation
Navigate to different routes programmatically after user actions.
const navigate = useNavigate();
navigate("/ui/cards");
navigate(-1); // Go backuseNavigation - Loading States
Track navigation state to show loading indicators during route transitions.
const navigation = useNavigation();
// navigation.state: "idle" | "loading" | "submitting"
{navigation.state === "loading" && <Spinner />}useLocation - Current Location
Access the current URL location information.
const location = useLocation(); location.pathname // "/ui/react" location.search // "?tab=navigation" location.hash // "#section"
useSearchParams - Query Strings
Read and update URL search parameters (query strings).
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get("tab");
setSearchParams({ tab: "new-value" });Demo 5: Loaders & Data LoadingServer-Side
Loaders - Server-Side Data Fetching
Loaders run on the server before the route renders. They fetch data from databases, APIs, or other sources.
// In route file
export async function loader({ context }: LoaderArgs) {
const data = await context.cloudflare.env.DB
.prepare("SELECT * FROM jobs")
.all();
return { jobs: data.results };
}
// In component
export default function Jobs() {
const { jobs } = useLoaderData<typeof loader>();
return <div>{jobs.map(job => ...)}</div>;
}Key Benefits:
- ✅ Runs on server (Cloudflare Workers)
- ✅ Data available before render (no loading state)
- ✅ Type-safe with TypeScript
- ✅ Automatic revalidation on navigation
Demo 6: Actions & FormsInteractive
Form Component - Enhanced Forms
The Form component provides progressive enhancement. It works without JavaScript and enhances with it.
Example Form (Demo Only)
// Action handler
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
// Process data, save to DB, etc.
await saveToDatabase(email);
return data({ success: true });
}
// In component
<Form method="post">
<input name="email" />
<button type="submit">Submit</button>
</Form>Demo 7: useFetcherInteractive
useFetcher - Non-Navigation Mutations
useFetcher lets you interact with actions without causing navigation. Perfect for like buttons, add to cart, etc.
Click the like button. Notice it updates immediately (optimistic UI) and shows saving state.
const fetcher = useFetcher();
// Submit without navigation
<fetcher.Form method="post" action="/api/like">
<button type="submit">
<FaHeart /> Like
</button>
</fetcher.Form>
// Check state
{fetcher.state === "submitting" && <Spinner />}
// Or use fetcher.submit()
fetcher.submit(
{ postId: "123" },
{ method: "post", action: "/api/like" }
);Demo 8: Optimistic UIBest Practice
Optimistic Updates - Better UX
Update the UI immediately while the request is in flight. Revert if it fails.
Pattern:
- 1. User clicks button
- 2. Update UI immediately (optimistic)
- 3. Send request to server
- 4. If success: Keep optimistic update
- 5. If error: Revert and show error
const fetcher = useFetcher();
// Optimistic data
const optimisticData = fetcher.formData
? processOptimistic(fetcher.formData)
: actualData;
// Display optimistic data
<div>
{optimisticData.map(item => (
<Item key={item.id} {...item} />
))}
</div>Demo 9: Error BoundariesError Handling
Route-Level Error Handling
Error boundaries catch errors in loaders, actions, and components. Display custom error pages.
Example Error Page
Oops! Something went wrong while loading this page.
// In route file
export function ErrorBoundary() {
const error = useRouteError();
return (
<div>
<h1>Error!</h1>
<p>{error.message}</p>
<button onClick={() => navigate("/")}>
Go Home
</button>
</div>
);
}
// Errors in loader/action are caught
export async function loader() {
const data = await fetchData();
if (!data) {
throw new Response("Not Found", { status: 404 });
}
return { data };
}