Pending and optimistic UI
While the server runs a loader or an action, the user should see that something is happening, otherwise they click the button again. The useNavigation hook tells you whether a navigation or a form submission is in progress. Optimistic UI goes further: it shows the result right away without waiting for the response.
Navigation states
useNavigation() returns an object whose main field is state:
state | What is happening |
|---|---|
idle | Nothing, the page is ready |
submitting | A form was submitted, the action is running |
loading | The new page's data is loading |
The button while submitting
// app/routes/new-post.tsx
import { Form, useNavigation } from "react-router";
export default function NewPost() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input name="title" placeholder="Title" />
<textarea name="body" placeholder="Post text" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Publishing..." : "Publish"}
</button>
</Form>
);
}After the click the button becomes disabled with the text "Publishing...", and once the server answers the post page opens. On a fast machine this takes an instant; throttle the network in the Network tab of the developer tools to see the label.
A page loading indicator
A shared indicator fits well in the layout app/routes/layout.tsx:
// app/routes/layout.tsx
import { Outlet, useNavigation } from "react-router";
export default function Layout() {
const navigation = useNavigation();
const isLoading = navigation.state !== "idle";
return (
<>
<header>My blog {isLoading && <span>Loading...</span>}</header>
<main style={{ opacity: isLoading ? 0.5 : 1 }}>
<Outlet />
</main>
</>
);
}On every navigation "Loading..." appears in the header, and the old page turns semi-transparent until the new data arrives.
Optimistic UI
During a submission navigation.formData holds the submitted fields. You can show them right away:
// app/routes/new-post.tsx
const title = navigation.formData?.get("title");
{title && <p>Publishing: {String(title)}</p>}The user sees the new post's title before the server responds. If the server returns an error, formData disappears, and the validation errors render instead of this line.
Common mistake
Checking navigation.state === "submitting" in the layout to show "Submitting form" only on a particular page. The state is shared by the whole app: it becomes submitting when any form is submitted. To tell your form apart, also compare navigation.formAction with the address the form posts to — or use useFetcher from the next lesson, which has its own state.