Skip to content
TaeyoungKim.dev

React Router loader: Fetch route data and handle errors at the route boundary

WebWritten 3 min readTaeyoungKim
LinkedInX

Fetching data in a component's useEffect leaves each screen to manage its own loading state, errors, and outdated requests. In React Router Data Mode, a route loader can own the data needed when entering that route.

A loader starts data work for a route

The diagram separates a successful loader result from a thrown 404. The example uses a server-free data: URL for success; a real API needs an explicit HTTP status check.

jsx
import {
  createBrowserRouter,
  RouterProvider,
  useLoaderData,
  useRouteError,
} from "react-router";

function PostPage() {
  const post = useLoaderData();
  return <h1>{post.title}</h1>;
}

function RouteError() {
  const error = useRouteError();
  return <p>{error?.status === 404 ? "Post not found" : "Please try again"}</p>;
}

const router = createBrowserRouter([
  {
    path: "/posts/:postId",
    element: <PostPage />,
    loader: async ({ params, request }) => {
      if (params.postId !== "1") {
        throw new Response("Not found", { status: 404 });
      }
      const demoUrl = "data:application/json," +
        encodeURIComponent(JSON.stringify({ title: "First post" }));
      const response = await fetch(demoUrl, { signal: request.signal });
      return response.json();
    },
    errorElement: <RouteError />,
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

/posts/1 shows a title; /posts/2 renders the error element. The data: URL demonstrates the fetch flow without a server. With a real API, check response.ok and throw for failed statuses instead of passing an error body to the component as valid data. Pass the loader's request.signal to fetch so an interrupted navigation can stop unneeded client work. React Router's Data Loading documentation shows how loader data reaches the route component.

Show errors at the route boundary

Hiding a 404 or server error as an empty list or null removes the reason for the failure. Throw a status-aware error from the loader and show a safe message and recovery path in the route's error element. Do not print raw internal server errors or private details to the page.

If a direct page refresh gives a server 404 before the app loads, inspect the hosting server's SPA route fallback. If the app loads and then shows the route error element, inspect the loader and the network response instead. React Router's error-boundary guidance explains the boundary for thrown loader errors.

A loader does not replace every kind of state

A filter that changes instantly with user input or temporary modal state can belong in a component. Use a loader for data tied to the URL and route entry. Keep server-changing work in an action or a clearly defined API flow.

Distinguish success, missing data, and server failure

Test a real /posts/:postId API with successful JSON, 404, and 500 responses. Successful data should reach useLoaderData(); failed responses should reach the error element. Do not display every failure as “not found.” A small helper makes the distinction explicit:

js
async function readPostResponse(response) {
  if (response.status === 404) throw new Response("Post not found", { status: 404 });
  if (!response.ok) throw new Response("Could not load post", { status: response.status });
  return response.json();
}

Also check cancellation during navigation. The server must enforce read authorization independently of the client-side loader.

Key takeaways

A loader prepares route data and connects to the router's cancellation and error-handling flow. Handle success, 404, and other server failures explicitly. Keep temporary UI state in the component when it is not part of the route's data lifetime.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

Read next