Skip to main content

useNavigation

Returns the current navigation state, defaulting to an “idle” navigation when no navigation is in progress. You can use this to render pending UI (like a global spinner) or read FormData from a form navigation.
This hook only works in Data and Framework modes.

Signature

function useNavigation(): Navigation

interface Navigation {
  state: "idle" | "loading" | "submitting";
  location?: Location;
  formData?: FormData;
  formAction?: string;
  formMethod?: "get" | "post" | "put" | "patch" | "delete";
  formEncType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "application/json";
}

Parameters

None.

Returns

navigation
Navigation
An object describing the current navigation:

Usage

Global loading indicator

import { useNavigation } from "react-router";

function GlobalSpinner() {
  const navigation = useNavigation();
  
  return navigation.state !== "idle" ? (
    <div className="spinner">Loading...</div>
  ) : null;
}

Show loading bar

function LoadingBar() {
  const navigation = useNavigation();
  const isLoading = navigation.state !== "idle";
  
  return (
    <div
      className="loading-bar"
      style={{
        opacity: isLoading ? 1 : 0,
        width: isLoading ? "100%" : "0%",
      }}
    />
  );
}

Disable UI during navigation

function NavigationBlocker() {
  const navigation = useNavigation();
  
  return (
    <div>
      <nav>
        <Link to="/page1">Page 1</Link>
        <Link to="/page2">Page 2</Link>
      </nav>
      
      {navigation.state !== "idle" && (
        <div className="overlay">
          <p>Navigating...</p>
        </div>
      )}
    </div>
  );
}

Show submitting state

function SubmitButton() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";
  
  return (
    <button type="submit" disabled={isSubmitting}>
      {isSubmitting ? "Saving..." : "Save"}
    </button>
  );
}

Optimistic UI with form data

function TodoList({ todos }) {
  const navigation = useNavigation();
  
  // Get the optimistic todo from form submission
  const optimisticTodo = navigation.formData
    ? {
        id: "temp",
        title: navigation.formData.get("title"),
        pending: true,
      }
    : null;
  
  const allTodos = optimisticTodo
    ? [optimisticTodo, ...todos]
    : todos;
  
  return (
    <ul>
      {allTodos.map((todo) => (
        <li key={todo.id}>
          {todo.title}
          {todo.pending && " (saving...)"}
        </li>
      ))}
    </ul>
  );
}

Show destination

function NavigationStatus() {
  const navigation = useNavigation();
  
  if (navigation.state === "loading" && navigation.location) {
    return (
      <p>Navigating to {navigation.location.pathname}...</p>
    );
  }
  
  return null;
}

Common Patterns

Different states for loading and submitting

function StatusIndicator() {
  const navigation = useNavigation();
  
  if (navigation.state === "submitting") {
    return <div>Saving...</div>;
  }
  
  if (navigation.state === "loading") {
    return <div>Loading...</div>;
  }
  
  return null;
}

Detect specific action

function DeleteIndicator() {
  const navigation = useNavigation();
  
  const isDeleting =
    navigation.state === "submitting" &&
    navigation.formData?.get("intent") === "delete";
  
  if (!isDeleting) return null;
  
  return <div className="alert">Deleting...</div>;
}

Loading skeleton

function Content({ children }) {
  const navigation = useNavigation();
  const isNavigating = navigation.state === "loading";
  
  return (
    <div className={isNavigating ? "loading" : ""}>
      {isNavigating ? <Skeleton /> : children}
    </div>
  );
}

Page transition animation

function PageTransition({ children }) {
  const navigation = useNavigation();
  
  return (
    <div
      className="page"
      style={{
        opacity: navigation.state === "loading" ? 0.5 : 1,
        transition: "opacity 200ms",
      }}
    >
      {children}
    </div>
  );
}
function NavigationProgress() {
  const navigation = useNavigation();
  const [progress, setProgress] = useState(0);
  
  useEffect(() => {
    if (navigation.state === "loading") {
      setProgress(0);
      const timer = setInterval(() => {
        setProgress((p) => Math.min(p + 10, 90));
      }, 200);
      return () => clearInterval(timer);
    } else {
      setProgress(100);
      setTimeout(() => setProgress(0), 200);
    }
  }, [navigation.state]);
  
  if (progress === 0) return null;
  
  return (
    <div
      className="progress-bar"
      style={{ width: `${progress}%` }}
    />
  );
}
function NavLink({ to, children, ...props }) {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";
  
  return (
    <Link
      to={to}
      {...props}
      style={{
        pointerEvents: isNavigating ? "none" : "auto",
        opacity: isNavigating ? 0.6 : 1,
      }}
    >
      {children}
    </Link>
  );
}

Form-specific loading

function ContactForm() {
  const navigation = useNavigation();
  
  const isSubmittingContact =
    navigation.state === "submitting" &&
    navigation.formAction === "/contact";
  
  return (
    <Form method="post" action="/contact">
      <input type="email" name="email" />
      <button type="submit" disabled={isSubmittingContact}>
        {isSubmittingContact ? "Sending..." : "Send"}
      </button>
    </Form>
  );
}
User clicks link/submits form

  state: "submitting"  (for POST/PUT/PATCH/DELETE)
  formData is available

    Action runs

  state: "loading"
  location is available

   Loaders run

  state: "idle"
  Page renders

Type Safety

import { useNavigation } from "react-router";

function Component() {
  const navigation = useNavigation();
  
  // Type guards for state
  if (navigation.state === "submitting") {
    // formData is definitely available
    const intent = navigation.formData?.get("intent");
  }
  
  if (navigation.state === "loading") {
    // location is definitely available
    const path = navigation.location?.pathname;
  }
}

Important Notes

Global state

useNavigation tracks the global navigation state. For component-specific operations that shouldn’t navigate, use useFetcher.

Idle state

When no navigation is in progress, state is "idle" and the other properties are undefined.

FormData availability

formData is only available during the "submitting" state for forms using POST, PUT, PATCH, or DELETE methods. GET form submissions go directly to "loading" state.

Build docs developers (and LLMs) love