Skip to content

Remix Pending UI — Loading States During Navigation

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Remix Pending UI. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Remix pending UI: show loading indicators during page transitions, loader submissions, and action processing for better user experience.

In this lesson, you'll use useNavigation() to detect pending navigation, show loading states, and create smooth transition experiences.

What You'll Learn

How to detect pending navigation, show loading indicators, disable buttons during submission, and implement progress bars.

Why It Matters

Users need feedback that their action is being processed. Pending UI prevents confusion, double-submissions, and makes the app feel responsive.

Real-World Use

DodaZIP shows a global loading bar at the top during navigation and disables form buttons during submission.

flowchart LR
    A[User Clicks] --> B{Navigation State}
    B -->|Idle| C[Normal UI]
    B -->|Submitting| D[Show Loading]
    B -->|Loading| E[Show Progress]
    D --> F[Action Complete]
    E --> G[Page Loaded]
    style B fill:#121212,color:#fff
import { useNavigation } from "@remix-run/react";

export default function GlobalLoading() {
  const navigation = useNavigation();
  const isPending = navigation.state !== "idle";

  if (!isPending) return null;

  return (
    <div style={{
      position: "fixed",
      top: 0,
      left: 0,
      right: 0,
      height: 3,
      background: "blue",
      transition: "opacity 0.2s",
    }} />
  );
}

Form Loading State

import { Form, useNavigation } from "@remix-run/react";

export default function ContactForm() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input name="email" disabled={isSubmitting} />
      <textarea name="message" disabled={isSubmitting} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Send Message"}
      </button>
    </Form>
  );
}

Loading Spinner Component

export function Spinner() {
  const navigation = useNavigation();
  const isPending = navigation.state !== "idle";

  return (
    <div
      style={{
        opacity: isPending ? 1 : 0,
        transition: "opacity 0.2s",
      }}
    >
      <div className="spinner" />
    </div>
  );
}

Per-Form Loading with useFetcher

import { useFetcher } from "@remix-run/react";

export default function SubscribeForm() {
  const fetcher = useFetcher();
  const isSubmitting = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post" action="/newsletter">
      <input name="email" disabled={isSubmitting} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Subscribing..." : "Subscribe"}
      </button>
    </fetcher.Form>
  );
}

Transition Location

Know where the user is navigating:

export default function NavigationLink({ to, children }) {
  const navigation = useNavigation();
  const isLoading = navigation.location?.pathname === to;

  return (
    <a
      href={to}
      style={{ opacity: isLoading ? 0.5 : 1 }}
    >
      {children}
    </a>
  );
}

Common Mistakes

  1. Not disabling form buttons during submission: Without disable, users can double-click and submit the form twice.
  2. Showing loading for every navigation: Only show loading for actual data-fetching navigations. Link clicks to the same page shouldn't show loading.
  3. Replacing content with spinner on navigation: The current content should stay visible while the next page loads, not replaced by a spinner.
  4. Not handling fetcher errors in pending UI: If a fetcher errors, reset the loading state and show the error.
  5. Over-animating transitions: Brief loading states (under 300ms) don't need animation. Show loading only for perceptible delays.

Practice Questions

  1. What hook tracks the current navigation state? Answer: useNavigation(). It returns { state, location, formData, formAction, formMethod }.

  2. What values can navigation.state have? Answer: "idle" (no navigation), "submitting" (form being submitted), "loading" (loader data loading).

  3. How do you disable a form during submission? Answer: Check navigation.state === "submitting" and set disabled on inputs and buttons.

  4. What is navigation.location used for? Answer: It tells you where the user is navigating to, useful for highlighting the active link.

Challenge

Build a navigation system that shows a global progress bar during page transitions, disables all links during navigation, and indicates which link is currently loading.

Mini Project

Create a multi-form settings page where each form has its own submit button. Each form should show its own loading state without affecting the others. Use useFetcher for independent form submissions.

FAQ

Can I use pending UI with JavaScript disabled?

: No. Pending UI requires JavaScript. The native browser loading indicator serves as feedback.

How do I prevent double submissions on the server?

: Use a unique submission token stored in the session. Reject duplicate tokens.

Should I show loading for all loader calls?

: No. Loaders that complete quickly (<300ms) don't need loading indicators. Add a minimum delay to prevent flicker.

How do I handle loading states for parallel fetches?

: Each fetcher has its own state. Track them independently for per-section loading.

What's Next

Learn about Remix Deferred Loading for streaming data and progressive rendering.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro