React Router v7 APIs

Demonstrating lazy loading, Suspense, and self-contained components

What is Lazy Loading?

Code Splitting

Lazy loading splits your app into smaller chunks that load on-demand, improving initial page load time. Components are downloaded only when needed, not on initial page load.

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!

Loaded 0 times

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

Smaller initial bundle means faster page load. Components download only when needed.

📦 Better Code Splitting

Self-contained components bundle perfectly - all related code loads together.

🎯 On-Demand Loading

Modals, drawers, and heavy components load only when user interacts with them.

🚀 Cloudflare Edge

Optimized for Cloudflare Workers deployment with efficient chunking.

💪 Type Safety

Full TypeScript support maintained even with lazy loading.

🎨 Better UX

Suspense fallbacks provide smooth loading experience for users.

Demo 4: Navigation APIsInteractive

useNavigate - Programmatic Navigation

Navigate to different routes programmatically after user actions.

const navigate = useNavigate();
navigate("/ui/cards");
navigate(-1); // Go back

useNavigation - Loading States

Track navigation state to show loading indicators during route transitions.

Current State:idle
const navigation = useNavigation();
// navigation.state: "idle" | "loading" | "submitting"

{navigation.state === "loading" && <Spinner />}

useLocation - Current Location

Access the current URL location information.

Pathname: /ui/react
Search: (none)
Hash: (none)
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).

Current tab param: (none)
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.

42

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. 1. User clicks button
  2. 2. Update UI immediately (optimistic)
  3. 3. Send request to server
  4. 4. If success: Keep optimistic update
  5. 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 };
}