React Router v7 Review
repository associated with this article: react-router-v7-review
Enhancing React Router Apps with Optimistic UI, Error Boundaries, and More
When working with React Router, improving responsiveness and user experience is key. In this article, we explore advanced techniques such as useFetcher, Optimistic UI updates, client-side redirects, and handling errors gracefully with an ErrorBoundary. Let's dive in!
Using useFetcher for Background Requests
The useFetcher hook lets us submit forms without navigation, keeping the UI fluid while interacting with loaders and actions.
const fetcher = useFetcher();
This is particularly useful when updating small parts of the UI without causing a full page reload. Let's see how we can apply it.
Optimistic UI for Better UX
By default, when we click a button (e.g., to favorite a contact), the UI might feel unresponsive because it's waiting for a server response. To solve this, we can use Optimistic UI updates, which immediately reflect changes before the network request completes.
const favorite = fetcher.formData ? fetcher.formData.get("favorite") === "true" : contact.favorite;
Here’s what happens:
- User clicks the favorite button.
- UI updates instantly to reflect the new state.
- If the network request fails, the UI reverts to the actual data.
This makes the app feel snappier and more responsive.
Automatic Revalidation After Actions
React Router automatically revalidates data after an action call, ensuring the UI remains in sync.
export async function action({ params, request }: Route.ActionArgs) { const formData = await request.formData(); const updates = Object.fromEntries(formData); await updateContact(params.contactId, updates); return redirect(`/contacts/${params.contactId}`); }
This also ensures a client-side redirect, preserving scroll positions and component state.
Creating a New Contact with an Action
Actions receive form data from a submission, allowing us to create new contacts without disrupting the user experience.
export async function action() { const contact = await createEmptyContact(); return redirect(`/contacts/${contact.id}/edit`); }
This prevents unnecessary page reloads while handling new form entries seamlessly.
Structuring the Root Layout
React Router provides special layout exports to maintain a consistent structure across the application.
export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href={appStylesHref} /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); }
This helps with hydration, scripts, and scroll restoration in the app.
Hydration Fallback for Better UX
Before the app fully loads on the client, we can provide a fallback UI to improve the user experience.
export function HydrateFallback() { return ( <div id="loading-splash"> <div id="loading-splash-spinner" /> <p>Loading, please wait...</p> </div> ); }
This ensures users see a loading indicator instead of a blank screen.
Global Error Handling with ErrorBoundary
Errors happen, and React Router provides an ErrorBoundary to catch all unexpected issues gracefully.
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { let message = "Oops!"; let details = "An unexpected error occurred."; let stack: string | undefined; if (isRouteErrorResponse(error)) { message = error.status === 404 ? "404" : "Error"; details = error.status === 404 ? "The requested page could not be found." : error.statusText || details; } else if (import.meta.env.DEV && error instanceof Error) { details = error.message; stack = error.stack; } return ( <main id="error-page"> <h1>{message}</h1> <p>{details}</p> {stack && ( <pre> <code>{stack}</code> </pre> )} </main> ); }
This makes error debugging easier and user-friendly.
React Router and SSR
While React Router is primarily used for client-side apps, it can also be configured for server-side rendering (SSR) and static pre-rendering.
export default { ssr: true, prerender: ["/about"], } satisfies Config;
This allows pages like /about to be pre-rendered at build time for better performance.
Final Thoughts
React Router provides powerful tools to improve user experience and maintain performance in single-page applications. By leveraging useFetcher, Optimistic UI, ErrorBoundary, and SSR strategies, we can create more seamless and efficient web apps.
Want to explore more? Check out the React Router documentation.