Thursday, June 19, 2025

useFetcher() Hook in React Router With Example

In this post we'll see how to use useFetcher() hook in React Router. Generally, when we have a loader or action function any call to it also triggers a navigation.

For example, suppose we have a Form component defined with method and action prop as given below.

<Form method="post" action="/favPost">

If you are at "/post" path when this action is triggered then you will be navigated to "/favPost" path.

If you don't want to trigger this navigation; meaning you want to call the action associated with "/favPost" path but need to stay at the current path "/post" that can be done using useFetcher() hook in React Router.

The useFetcher hook in React Router allows for execution of route loaders and actions without triggering a full navigation. Any scenario where data has to be loaded or mutated in background, paginated data where you want to stay on the same page with next page data can be handled using useFetcher.

useFetcher hook in React

useFetcher hook returns an object.

const fetcher = useFetcher();

Some of the properties provided by the fetcher object are as-

  1. fetcher.state- Available states are "idle", "loading", "submitting".
  2. fetcher.data- The data returned from the called action or loader
  3. fetcher.Form- Form used to submit data to a route action without navigating away from the current page.
  4. fetcher.submit- To programmatically submit data to a route action without navigating away from the current page.
  5. fetcher.load- Fetching data from a loader function associated with a route without navigating away from the current page.

useFetcher hook React example

Suppose you have a scenario to show list of posts and also have an input element at the top to take user input for favourite post. While saving that favourite post you don't want to navigate to the "/favPost" route but want to stay at the post list page.

useFetcher() Hook in React Router

Route Configuration

import { createBrowserRouter } from "react-router";
import Home from "./home";
import NavigationNavLink from "./NavigationNavLink";
import ErrorPage from "./ErrorPage";
import PostList, {loader as postLoader} from "./PostList";
import PostLayout from "./PostLayout";
import FavPost, {action as favPostAction} from "./FavPost";


export const route = createBrowserRouter([
  {path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,
   children: [
    {index: true, element: <Home /> },
    {path: "post", element: <PostLayout />,
     children:[
        {index: true, element: <PostList />, loader: postLoader, hydrateFallbackElement: <h2>Loading...</h2>},
     ]
    },
    {path: "favpost", element: <FavPost />, action: favPostAction}
   ]
  },
])

Components

For Navigation Menu, Home and ErrorPage components you can get the code from this post- Data Loader in React Router

src\components\routes\PostLayout.js

This component acts as a parent route component for all the Post related functionality. Child component is rendered in the place of Outlet which acts as a placeholder.

import { Outlet } from "react-router";
const PostLayout = () => {
  return(
    <div className="mx-2">
      <Outlet />
    </div>
  )
}

export default PostLayout;

src\components\routes\PostList.js

This component lists all the fetched post data.

import { useLoaderData } from "react-router";
import FavPost from "./FavPost";

const PostList = () => {
  const postData = useLoaderData();

  return(
    <> 
      <FavPost />
      <h2 className="text-info-emphasis text-center">Posts</h2>
      <ul className="list-group">
        {postData.map((post) =>
        <li className="list-group-item" key={post.id}>
            {post.id} {post.title}
        </li>
        )}
      </ul>
    </>
  )
}

export default PostList


export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
    // use Response object
    throw new Response("Failed to load data", { status: response.status });
  }else{
    const responseData = await response.json();
    return responseData;
  }
}

As you can see <FavPost> component is called with in the <PostList> component.

src\components\routes\FavPost.js

import { useEffect } from "react";
import { useFetcher } from "react-router";

const FavPost = () => {
  const fetcher = useFetcher();
  const {data, state} = fetcher;
  useEffect(()=>{
    if(state === "idle" && data && data.message){
        window.alert(data.message);
    }
  }, [data, state])
  return(
    <fetcher.Form method="post" action="/favPost">
      <label className="form-label" htmlFor="fav">Your favorite post</label>
      <input type="text" id="fav" name="favpost"></input>
      <button className="btn btn-success" type="submit">Save</button> 
    </fetcher.Form> 
  );
}

export default FavPost;

export async function action({request}){
  console.log('Saving favorite post data')
  const data = await request.formData();
  const favpost = data.get('favpost');
  // send to backend server to save favorite post...
  console.log(favpost);
  return { message: 'Saved successfully!' };
}

Some of the points to note here-

  1. useFetcher is used here to avoid navigation away from the current path.
  2. Using object destructuring data and state from fetcher are extracted to two variables.
  3. With in useEffect() hook there is a check that the state is “idle” (action execution is done), there is data then show the data.message.
  4. It is actually fetcher.Form which causes the action function to be called without navigating away from the current page. By changing it to
    you can see that it navigates to the “/favPost” path then.

That's all for this topic useFetcher() Hook in React Router With Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. CRUD Example With React Router Loader and Action
  2. Data Loader With Dynamic Routes in React
  3. Actions in React Router For Data Mutation
  4. useRouteLoaderData() Hook in React Router
  5. useSearchParams in React Router - Handling Query Parameters

You may also like-

  1. JVM Run-Time Data Areas - Java Memory Allocation
  2. Difference Between Abstract Class And Interface in Java
  3. Armstrong Number or Not Java Program
  4. Spring Boot + Data JPA + MySQL REST API CRUD Example

CRUD Example With React Router Loader and Action

In the post- Actions in React Router For Data Mutation we saw how to handle data mutation and add a new record using action function. In this post we'll see a complete CRUD example using loader and action in React Router.

Add, update, delete and fetch data using React Router

This example shows how you can load post data and also how you can do post data mutation (adding, modifying or deleting). For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling.

Routing Definition

Routing configuration for the CRUD example is as given below.

src\components\routes\Route.js

import { createBrowserRouter } from "react-router";
import Home from "./home";
import NavigationNavLink from "./NavigationNavLink";
import ErrorPage from "./ErrorPage";
import PostList, {loader as postLoader} from "./PostList";
import PostDetails, {loader as postDetailsLoader} from "./PostDetails";
import PostLayout from "./PostLayout";
import PostEdit from "./PostEdit";
import PostAdd from "./PostAdd";
import {addEditAction as postAddEditAction, deleteAction as postDeleteAction} from "./PostActions";

export const route = createBrowserRouter([
  {path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,
   children: [
    {index: true, element: <Home /> },
    {path: "post", element: <PostLayout />,
     children:[
      {index: true, element: <PostList />, loader: postLoader, hydrateFallbackElement: <h2>Loading...</h2>},
      {path: ":postId", id: "post-detail", loader: postDetailsLoader,
       children: [
          {index:true, element: <PostDetails />, action: postDeleteAction, hydrateFallbackElement: <h2>Loading...</h2>},
          {path:"edit", element: <PostEdit />, action: postAddEditAction},
       ]
      },
      {path:"new", element: <PostAdd />, action: postAddEditAction}
     ]
    },
   ]
  },
])

Here we have 3 levels of nested routes. Root parent is Navigation menu component. Another level is for PostLayout component for path "/post" which renders the list of posts, yet another level of nested route is for dynamic path "/post/POST_ID" where we have operations to fetch, delete or edit post by ID.

Path "/post/new" is for creating a resource.

Notice the use of loader and action properties to specify the needed loader and action functions.

Components

For Navigation Menu, Home and ErrorPage components you can get the code from this post- Data Loader in React Router

src\components\routes\PostLayout.js

This component acts as a parent route component for all the Post related functionality. Child component is rendered in the place of Outlet which acts as a placeholder.

import { Outlet } from "react-router";
const PostLayout = () => {
  return(
    <div className="mx-2">
      <Outlet />
    </div>
  )
}

export default PostLayout;

src\components\routes\PostList.js

This component lists all the fetched posts data.

import { Link, useLoaderData, useNavigate } from "react-router";

const PostList = () => {
  const postData = useLoaderData();
  const navigate = useNavigate();
  const PostAddHandler = () => {
    navigate("./new");
  }
  return(
    <> 
      <div>
        <button className="btn btn-info" onClick={PostAddHandler}>Add New Post</button>
      </div>

      <h2 className="text-info-emphasis text-center">Posts</h2>
      <ul className="list-group">
        {postData.map((post) =>
        <li className="list-group-item" key={post.id}>
          {post.id} <Link to={post.id.toString()}>{post.title}</Link>  
        </li>
        )}
      </ul>
    </>
  )
}

export default PostList


export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
    // use Response object
    throw new Response("Failed to load data", { status: response.status });
  }else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written.
  2. Uses fetch API to get data from the given URL. Only 10 posts are fetched.
  3. Data fetched by the loader function is retrieved using the useLoaderData() hook.
  4. That post data is then iterated and link with postID is created with each post so that clicking any post leads to rendering of details for the post.
  5. There is also a button to add new post. Clicking that button results in navigation to “/new” relative to current route path.

src\components\routes\PostDetails.js

This component renders the post data by ID.

import { Form, useNavigate, useRouteLoaderData, useSubmit } from "react-router";

const PostDetails = () => {
  const postData = useRouteLoaderData("post-detail");
  const navigate = useNavigate();
  const submit = useSubmit();
  const editHandler = () => {
    navigate("./edit");
  }
  const deleteHandler = () => {
    const flag = window.confirm("Do you really want to delete");
    if(flag){
      submit(null, {method:"DELETE"});
    }
  }
  return(
    <>
      <h2 className="text-center">{postData.title}</h2>   
      <div>{postData.body}</div>
      <div className="text-center">
        <button className="btn btn-success" onClick={editHandler}>Edit</button> 
        <button className="btn btn-danger mx-2" onClick={deleteHandler}>Delete</button>
      </div>
    </>
  )
}

export default PostDetails;

export async function loader({request, params}){
  const url = "https://jsonplaceholder.typicode.com/posts/"+params.postId
  const response = await fetch(url);
  //console.log(response);
  if (!response.ok) {
    throw new Response('Error while fetching post data', {status:404});
  }
  else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written which is used to fetch data by post Id.
  2. In loader function you can’t use useParams() hook to get the route parameters. Router passes an object to the loader function that has two properties- request and params.
  3. Using request you can access the request body, request URL etc. Using params you can access the route path parameters.
  4. Data fetched by the loader function is retrieved by using the useLoaderData() hook. In this case data is post data for a particular ID.
  5. Post title and post body are rendered by this component.
  6. There are two buttons also “edit” and “delete”. Clicking “edit” button results in route “/post/POST_ID/edit”. Clicking on “delete” button asks for confirmation first.
  7. If delete is confirmed then the delete request is submitted programmatically using useSubmit() hook. Read more about useSubmit() hook in this post- useSubmit() Hook in React Router
Update with React Router

src\components\routes\PostActions.js

This JavaScript file has action functions for add/edit and delete.

import { redirect } from "react-router";

export async function addEditAction({request, params}){
  const data = await request.formData();
  const postId = params.postId;
  const postData = {
    title:data.get("title"),
    body:data.get("body"),
    userId:data.get("userId"),
    // add id if required -- Only for edit
    ...(postId && { id: postId })
  } 
  //console.log('POST data', postData);
  const method = request.method;

  let url = 'https://jsonplaceholder.typicode.com/posts';
  if(method === "PUT" || method === "PATCH"){
    url = url+"/"+postId;
    console.log(url);
  }
  const response = await fetch(url, 
    {
      method: method, 
      headers: {                
        'Content-type': 'application/json',                 
      },
      body: JSON.stringify(postData)
    },
      
  );
  if(!response.ok){
      throw new Response('Error while saving post data', {status:500});
  } 
  const responseData = await response.json();
  console.log(responseData);
  return redirect('/post');
}

export async function deleteAction({request, params}){
  let url = 'https://jsonplaceholder.typicode.com/posts/'+params.postId;
  console.log('DELETE', url);
  const method = request.method;
  console.log('Method', method);
  const response = await fetch(url, {
    method: method
  });
  if(!response.ok){
    throw new Response('Error while deleting post data', {status:500});
  } 
  const responseData = await response.json();
  //console.log(responseData);
  return redirect('/post');
}

Some of the points to note here-

  1. Action function receives an object that has properties like request, params.
  2. Request object has a method formData() that returns form data.
  3. Using params you can access the route path parameters.
  4. In deleteAction function postID is extracted from the URL using params and added to the URL which is sent to delete the specific post.
  5. Fetch API is used to make HTTP requests.
  6. For adding and editing, same function is used with some conditional changes like post data that is sent as part of request body includes postID in case of edit where as for add it doesn’t include postID.
  7. URL also is created conditionally, in case of edit postID has to be added as path parameter
  8. With action function or loader function it is recommended to use redirect function rather than useNavigate to navigate to another page. That’s what is done here, if operation completes successfully, redirect to “/post” path.
Delete with React Router

src\components\routes\PostAdd.js

This component is called when the route path is "/post/new".

import PostForm from "./PostForm"

const PostAdd =() => {
  return(
    <PostForm method="POST"/>
  )
}

export default PostAdd;

src\components\routes\PostEdit.js

This component is rendered for editing post. In this component post data (by id) is retrieved using useRouteLoaderData() hook and that data is passed to another component PostForm as a prop.

Why useRouteLoaderData() hook is used here. To understand that refer this post- useRouteLoaderData() Hook in React Router

import { useRouteLoaderData } from "react-router";
import PostForm from "./PostForm";

const PostEdit = () => {
    const postData = useRouteLoaderData("post-detail");
    return(
        <PostForm postData={postData} method="PUT" />
    )
}

export default PostEdit;

src\components\routes\PostForm.js

Form component which is rendered for both adding or editing a post. When editing, Post form is pre-filled with existing post data.

import { Form, useNavigation } from "react-router";

const PostForm = ({postData, method}) => {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting';

  return(
    <div className="container">
      <h2>Post</h2>
      <Form method={method}> 
        {(method === "PUT" || method === "PATCH")?
        <div className="mb-2 mt-2">
          <label className="form-label" htmlFor="id">ID: </label>
          <input className="form-control" type="text" name="id" id="id" disabled={true} defaultValue={postData?postData.id:""}></input>
        </div>
        :""}
        <div className="mb-2">
          <label className="form-label" htmlFor="title">Title: </label>
          <input className="form-control" type="text" name="title" id="title" defaultValue={postData?postData.title:""}></input>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="body">Body: </label>
          <textarea className="form-control" type="text" name="body" id="body" rows="3" defaultValue={postData?postData.body:""}></textarea>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="userId">User ID: </label>
          <input className="form-control" type="text" name="userId" id="userId"  defaultValue={postData?postData.userId:""}></input>
        </div>
        <button className="btn btn-info" type="submit" disabled={isSubmitting}>Save</button>
      </Form>
    </div>
  )
}

export default PostForm;

Some of the points to note here-

  1. While using PostForm component, method is passed as a prop. In PostAdd method is passed as “POST” whereas with PostEdit it is passed as “PUT”.
  2. Whether “id” field should be displayed or not is decided on the basis of method. For add it is not displayed as new postID will be created for the newly added post. For edit existing postID is displayed.
  3. Navigation state is also accessed using the useNavigation() hook. Using that “Save” button is disabled once the form is submitted to avoid multiple clicks for the same post.
Edit form

That's all for this topic CRUD Example With React Router Loader and Action. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Data Loader With Dynamic Routes in React
  2. useRouteLoaderData() Hook in React Router
  3. Using NavLink in React Router
  4. Index Route in React Router

You may also like-

  1. JVM Run-Time Data Areas - Java Memory Allocation
  2. Difference Between Abstract Class And Interface in Java
  3. Armstrong Number or Not Java Program
  4. Spring Boot + Data JPA + MySQL REST API CRUD Example

Tuesday, June 17, 2025

useSubmit() Hook in React Router

In the post Actions in React Router For Data Mutation we saw how you can create a resource (POST method) using actions and Form component in React Router. Another way (more explicit way) to submit a form is to use useSubmit() hook in React Router. In this post we'll see how to programmatically submit a form using useSubmit() hook.

useSubmit() hook in React Router

useSubmit hook returns a function.
const submit = useSubmit();

This returned function (named submit here) takes two arguments. First is the data that has to be submitted which will be wrapped as form data object. Another argument is an optional object where you can pass method with which you can specify the HTTP verb to use when the form is submitted. Supports "get", "post", "put", "delete", and "patch". Another property you can pass is action with which you can provide the URL to submit the form data to. If no action is specified, this defaults to the closest route in context.

submit(event.currentTarget, {method:"POST", action:"/post/new"});

useSubmit() hook React example

In this example we'll use an action function to create a resource. For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling. This example is same as what is already shown in this post Actions in React Router For Data Mutation except the change for programmatically submitting the form having post data using useSubmit() hook.

Component that changes is the PostForm component. Now rather than using the Form component normal <form> element is used. On submitting the form handler function is called that submits the form programmatically.

src\components\routes\PostForm.js

import { useSubmit } from "react-router";

const PostForm = () => {
   
  const submit = useSubmit();
  const formSubmitHandler = (event) => {
    event.preventDefault();
    //console.log(event.currentTarget);
    submit(event.currentTarget, {method:"POST", action:"/post/new"});
  }
  return(
    <div className="container">
      <h2>Post</h2>
      <form onSubmit={formSubmitHandler}> 
        {/* <div className="mb-2 mt-2">
            <label className="form-label" htmlFor="id">ID: </label>
            <input className="form-control" type="text" name="id" id="id" defaultValue={postData?postData.id:""}></input>
        </div> */}
        <div className="mb-2">
          <label className="form-label" htmlFor="title">Title: </label>
          <input className="form-control" type="text" name="title" id="title"></input>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="body">Body: </label>
          <textarea className="form-control" type="text" name="body" id="body"></textarea>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="userId">User ID: </label>
          <input className="form-control" type="text" name="userId" id="userId" rows="3"></input>
        </div>
        <button className="btn btn-info" type="submit">Save</button>
      </form>
    </div>
  )
}

export default PostForm;

In the handler function (formSubmitHandler()) for form submit, function returned by useSubmit hook is used to submit data. In the submit function first argument is event.currentTarget which is the form itself. As the second argument an object is passed with method and action properties. Here method value is POST and action is "/post/new" path where the action function is defined. If we don't specify action path here that will also work because action defaults to the closest route in context which is anyway "/post/new".

src\components\routes\PostAdd.js

This component is called when the route path is "/post/new".

import { redirect } from "react-router";
import PostForm from "./PostForm"

const PostAdd =() => {
  return(
    <PostForm />
  )
}

export default PostAdd;

export async function action({request, params}){
  const data = await request.formData();
  const postData = {
    title:data.get("title"),
    body:data.get("body"),
    userId:data.get("userId")
  }
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', 
    {
      method: 'POST', 
      headers: {                
          'Content-type': 'application/json',                 
      },
      body: JSON.stringify(postData)
    },
      
  );
  if(!response.ok){
    throw new Response('Error while saving post data', {status:500});
  } 
  const responseData = await response.json();
  //console.log(responseData);
  return redirect('/post');
}

Some of the points to note here-

  1. Action function receives an object that has properties like request, params.
  2. Request object has a method formData() that returns form data.
  3. On that data object you can call get() method to get the form field value by passing the form field name. The name you pass in get method should match the name you gave in the input element with in the form. Refer PostForm to check the name attribute in each input element.
  4. With action function or loader function it is recommended to use redirect function rather than useNavigate to navigate to another page. That’s what is done here, if post is added successfully redirect to “/post” path.

That's all for this topic useSubmit() Hook in React Router. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Data Loader With Dynamic Routes in React
  2. useRouteLoaderData() Hook in React Router
  3. Using NavLink in React Router
  4. Index Route in React Router

You may also like-

  1. JVM Run-Time Data Areas - Java Memory Allocation
  2. Difference Between Abstract Class And Interface in Java
  3. Armstrong Number or Not Java Program
  4. Spring Boot + Data JPA + MySQL REST API CRUD Example

Actions in React Router For Data Mutation

In the post Data Loader in React Router we have seen how to fetch data using loaders in React Router. Same way action in React Router is a function that handles data mutations.

If you have a CRUD operation then Create, Delete and Update part (data mutation operations) can be done by actions. For read operation (data fetching) you can use loader.

How to call actions

You can execute action function by using one of the following ways.

  1. Actions are called declaratively through Form component provided by React Router. That is what we'll see in this article
  2. Imperatively through useSubmit() hook
  3. By using fetcher.Form or fetcher.submit

Using action to Post data- React Example

In this example we'll use an action function to create a resource. For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling.

Route configuration

export const route = createBrowserRouter([
  {path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,
   children: [
    {index: true, element: <Home /> },
    {path: "post", element: <PostLayout />,
     children:[
        {index: true, element: <PostList />, loader: postLoader, hydrateFallbackElement: <h2>Loading...</h2>},
        {path:"new", element: <PostAdd />, action: postAddAction}
     ]
    },
   ]
  },
])

Here we have 2 levels of nested routes. Root parent is Navigation menu component. Another level is for PostLayout component for path "/post" which renders the list of posts and another nested route definition for "/post/new" to create a new post.

Notice the use of action property with route definition for "/post/new".

{path:"new", element: <PostAdd />, action: postAddAction}

That points to the action function called to add post data.

Components

For Navigation Menu, Home and ErrorPage components you can get the code from this post- Data Loader in React Router.

src\components\routes\PostLayout.js

This component acts a parent route component for all the Post related functionality.

import { Outlet } from &quot;react-router&quot;;
const PostLayout = () =&gt; {
    return(
        &lt;div className=&quot;mx-2&quot;&gt;
            &lt;Outlet /&gt;
        &lt;/div&gt;

    )
}

export default PostLayout;

src\components\routes\PostList.js

This component lists all the fetched post data.

import { Link, useLoaderData, useNavigate } from "react-router";

const PostList = () => {
  const postData = useLoaderData();
  const navigate = useNavigate();
  const PostAddHandler = () => {
    navigate("./new");
  }
  return(
    <> 
      <button className="btn btn-info" onClick={PostAddHandler}>Add New Post</button>
      <h2 className="text-info-emphasis text-center">Posts</h2>
      <ul className="list-group">
        {postData.map((post) =>
        <li className="list-group-item" key={post.id}>
            {post.id} <Link to={post.id.toString()}>{post.title}</Link>  
        </li>
        )}
      </ul>
    </>
  )
}

export default PostList


export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
    // use Response object
    throw new Response("Failed to load data", { status: response.status });
  }else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written.
  2. Uses fetch API to get data from the given URL. Only 10 posts are fetched.
  3. Data fetched by the loader function is retrieved using the useLoaderData() hook. You can use useLoaderData here as loader data is needed with in the current route.
  4. There is also a button to add new post. Clicking that button results in navigation to “/new” relative to current route path.
Actions in React Router

src\components\routes\PostForm.js

This component renders a form to enter data for new post.

import { Form } from "react-router";

const PostForm = () => {
  return(
    <div className="container">
      <h2>Post</h2>
      <Form method="post"> 
        <div className="mb-2">
          <label className="form-label" htmlFor="title">Title: </label>
          <input className="form-control" type="text" name="title" id="title"></input>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="body">Body: </label>
          <textarea className="form-control" type="text" name="body" id="body"></textarea>
        </div>
        <div className="mb-2">
          <label className="form-label" htmlFor="userId">User ID: </label>
          <input className="form-control" type="text" name="userId" id="userId" rows="3"></input>
        </div>
        <button className="btn btn-info" type="submit">Save</button>
      </Form>
    </div>
  )
}

export default PostForm;

Some of the points to note here-

  1. Form component used here is provided by React Router which is a progressively enhanced HTML <form> that submits data to actions. When the form is submitted form data is provided to the action.
  2. With Form component you can use props like action and submit.
  3. With action prop you can provide the URL to submit the form data to. If no action is specified, this defaults to the closest route in context.
  4. With method prop you can specify the HTTP verb to use when the form is submitted. Supports "get", "post", "put", "delete", and "patch".

src\components\routes\PostAdd.js

This component is called when the route path is "/post/new".

import { redirect } from "react-router";
import PostForm from "./PostForm"

const PostAdd =() => {
  return(
    <PostForm />
  )
}

export default PostAdd;

export async function action({request, params}){
  const data = await request.formData();
  const postData = {
    title:data.get("title"),
    body:data.get("body"),
    userId:data.get("userId")
  }
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', 
    {
      method: 'POST', 
      headers: {                
          'Content-type': 'application/json',                 
      },
      body: JSON.stringify(postData)
    },
      
  );
  if(!response.ok){
    throw new Response('Error while saving post data', {status:500});
  } 
  const responseData = await response.json();
  console.log(responseData);
  return redirect('/post');
}

Some of the points to note here-

  1. Action function receives an object that has properties like request, params.
  2. Request object has a method formData() that returns form data.
  3. On that data object you can call get() method to get the form field value by passing the form field name. The name you pass in get method should match the name you gave in the input element with in the form. Refer PostForm to check the name attribute in each input element.
  4. With action function or loader function it is recommended to use redirect function rather than useNavigate to navigate to another page. That’s what is done here, if post is added successfully redirect to “/post” path.
data mutation in React Router

That's all for this topic Actions in React Router For Data Mutation. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. useRouteLoaderData() Hook in React Router
  2. Search Filter Using useSearchParams in React Router
  3. Setting 404 Error Page With React Router
  4. Lazy Loading Routes in React

You may also like-

  1. Controlled and Uncontrolled Components in React
  2. JavaScript Array slice() method With Examples
  3. Java Sealed Classes and Interfaces
  4. Java split() Method - Splitting a String
  5. Accessing Characters in Python String
  6. Angular Route Resolver - Passing Data Dynamically

useRouteLoaderData() Hook in React Router

In this post we'll see how to use useRouteLoaderData() hook in React Router to access data loaded by loader function. useRouteLoaderData() hook makes data available any where within the component hierarchy, that's how it differs from another similar hook useLoaderData() which can access data from the closest route loader.

The benefit of useRouteLoaderData() hook is that it enables sharing of data across components. Component can access data from parent route or sibling routes.

How to use useRouteLoaderData in React Router

You need to first import it.

import { useRouteLoaderData } from "react-router";

You need to pass route ID of the given route to useRouteLoaderData() and it returns loader data for that route. For example if you have defined route as given below with id property.

{path: ":postId", id: "post-detail", loader: postDetailsLoader}

Then you can use it like this with useRouteLoaderData to get data fetched by postDetailsLoader loader function.

const postData = useRouteLoaderData("post-detail");

Route IDs are created automatically too. They are simply the path of the route file relative to the app folder, so you can use, automatically created id also rather than creating one manually.

useRouteLoaderData() hook React example

For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling.

Let's try to understand how useRouteLoaderData() hook works with an example. Suppose we have the following route configuration.

export const route = createBrowserRouter([
  {path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,
   children: [
    {index: true, element: <Home /> },
    {path: "post", element: <PostLayout />,
     children:[
        {index: true, element: <PostList />, loader: PostLoader, hydrateFallbackElement: <h2>Loading...</h2>},
        {path: ":postId", id: "post-detail", loader:PostDetailsLoader,
           children: [
              {index:true, element: <PostDetails />, hydrateFallbackElement: <h2>Loading...</h2>},
              {path:"edit", element: <PostEdit />}
           ]
        },
        {path:"new", element: <PostAdd />, action:PostAddAction}
     ]
    },
   ]
  },
])

Here we have 3 levels of nesting. Root parent is Navigation menu component. Another level is for PostLayout component for path "/post" which renders the list of posts.

Yet another level is dynamic route for "/post/POST_ID" which renders details for the selected post through PostDetails component. There is another child with in the same hierarchy to render a form to edit the selected post, for that path is "post/POST_ID/edit".

When you are showing edit form for editing a post, you want that form pre-filled with the selected post data (fetch post by id). Same way when you are going to post details page that shows data for the selected post. Since both of these components need data by postId, so the loader function can be shared. If you notice the route configuration, that's what has been done, loader is configured at the parent level.

{path: ":postId", id: "post-detail", loader:PostDetailsLoader,
  children: [
    {index:true, element: <PostDetails />, hydrateFallbackElement: <h2>Loading...</h2>},
    {path:"edit", element: <PostEdit />}
  ]
},

With the above route configuration, trying to get loader data using useLoaderData() hook won't work as that can get data only from the closest route loader where as in this case we need data from the parent route. In this kind of scenario useRouteLoaderData() hook can be used as that can get data from the parent route.

Components

For Navigation Menu, Home and ErrorPage components you can get the code from this post- Data Loader in React Router.

src\components\routes\PostLayout.js

This component acts a parent route component for all the Post related functionality.

import { Outlet } from &quot;react-router&quot;;
const PostLayout = () =&gt; {
    return(
        &lt;div className=&quot;mx-2&quot;&gt;
            &lt;Outlet /&gt;
        &lt;/div&gt;

    )
}

export default PostLayout;

src\components\routes\PostList.js

This component lists all the fetched post data.

import { Link, useLoaderData } from "react-router";

const PostList = () => {
    const postData = useLoaderData();
    return(
      <> 
        <h2 className="text-info-emphasis text-center">Posts</h2>
        <ul className="list-group">
            {postData.map((post) =>
            <li className="list-group-item" key={post.id}>
                {post.id} <Link to={post.id.toString()}>{post.title}</Link>  
            </li>
            )}
        </ul>
      </>
    )
}

export default PostList

export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
    // use Response object
    throw new Response("Failed to load data", { status: response.status });
  }else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written.
  2. Uses fetch API to get data from the given URL. Only 10 posts are fetched.
  3. If there is no error in fetching data (response is ok) then you can return data directly with React Router V7.
  4. Data fetched by the loader function is retrieved by using the useLoaderData() hook. You can use useLoaderData here as loader data is needed with in the current route.
  5. Post data is then iterated to print id and title of each post. With each post a Link with its id is also created
useRouteLoaderData() Hook in React Router

src\components\routes\PostDetails.js

This component renders details of the post whose ID is selected. Also shows buttons to edit or delete post.

import { useNavigate, useRouteLoaderData } from "react-router";

const PostDetails = () => {
  const postData = useRouteLoaderData("post-detail");
  const navigate = useNavigate();
  const editHandler = () => {
      navigate("./edit");
  }
  return(
    <>
      <h2 className="text-center">{postData.title}</h2>
      <div>{postData.body}</div>
      <div className="text-center">
        <button className="btn btn-success " onClick={editHandler}>Edit</button> 
        <button className="btn btn-danger mx-2">Delete</button>
      </div>
    </>
  )
}

export default PostDetails;

export async function loader({request, params}){
  const url = "https://jsonplaceholder.typicode.com/posts/"+params.postId
  const response = await fetch(url);
  console.log(response);
  if (!response.ok) {
      throw new Response('Error while fetching post data', {status:404});
  }
  else{
      const responseData = await response.json();
      return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written which is used to fetch data by post Id.
  2. Data fetched by the loader function is retrieved using the useRouteLoaderData() hook. That is needed as loader though defined here is actually called at the parent route level
    {path: ":postId", id: "post-detail", loader: postDetailsLoader,
      children: [
        {index:true, element: <PostDetails />, hydrateFallbackElement:  <h2>Loading...</h2>},
    
  3. Clicking on Edit button navigates to /post/POST_ID/edit path
  4. Delete button functionality is not given in this post.
useRouteLoaderData() Hook

src\components\routes\PostEdit.js

This component retrieves the post data (by id) using useRouteLoaderData() hook and pass that data to another component PostForm as a prop.

import { useRouteLoaderData } from "react-router";
import PostForm from "./PostForm";

const PostEdit = () => {
  const postData = useRouteLoaderData("post-detail");
  return(
    <PostForm postData={postData} />
  )
}

export default PostEdit;
src\components\routes\PostForm.js
const PostForm = ({postData}) => {
  return(
    <div className="container">
      <h2>Post</h2>
      <form>
        <div className="mb-2 mt-2">
            <label className="form-label" htmlFor="id">ID: </label>
            <input className="form-control" type="text" name="id" id="id" defaultValue={postData?postData.id:""}></input>
        </div>
        <div className="mb-2">
            <label className="form-label" htmlFor="title">Title: </label>
            <input className="form-control" type="text" name="title" id="title" defaultValue={postData?postData.title:""}></input>
        </div>
        <div className="mb-2">
            <label className="form-label" htmlFor="body">Body: </label>
            <textarea className="form-control" type="text" name="body" id="body" defaultValue={postData?postData.body:""}></textarea>
        </div>
        <div className="mb-2">
            <label className="form-label" htmlFor="userId">User ID: </label>
            <input className="form-control" type="text" name="userId" id="userId" rows="3" defaultValue={postData?postData.userId:""}></input>
        </div>
        <button className="btn btn-info" type="submit">Save</button>
      </form>
    </div>
  )
}

export default PostForm;

That's all for this topic useRouteLoaderData() Hook in React Router. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Lazy Loading Routes in React
  2. Data Loader in React Router
  3. Data Loader With Dynamic Routes in React
  4. React Declarative Approach

You may also like-

  1. JavaScript Rest Parameter
  2. Spring Boot Observability - Distributed Tracing, Metrics
  3. Java Record Class With Examples
  4. Interface in Java With Examples
  5. Angular CanActivateChild Guard to protect Child Routes

Thursday, June 12, 2025

Data Loader With Dynamic Routes in React

In the post Data Loader in React Router we saw how to use loader function to fetch data for route component. In this post we'll see how to use data loader with dynamic routes in React.

Data Loading React Router Example

In this example we'll use a loader function to fetch data from API. For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling.

Route Configuration

src\components\routes\Route.js

import { createBrowserRouter } from "react-router";
import Home from "./home";
import NavigationNavLink from "./NavigationNavLink";
import ErrorPage from "./ErrorPage";
import PostList, {loader as PostLoader} from "./PostList";
import PostDetails, {loader as PostDetailsLoader} from "./PostDetails";
import PostLayout from "./PostLayout";

export const route = createBrowserRouter([
  {path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,
   children: [
    {index: true, element: <Home /> },
    {path: "post", element: <PostLayout />,
     children:[
        {index: true, element: <PostList />, loader: PostLoader, hydrateFallbackElement: <h2>Loading...</h2>},
        {path: ":postId", element: <PostDetails />, loader: PostDetailsLoader, hydrateFallbackElement: <h2>Loading...</h2>},
     ]
    },
   ]
  },
])

As you can see there is a Navigation Menu component that is mapped to root route. Nested routes of root route are Home and PostLayout.

PostLayout again has nested routes for PostList to show all the posts and another as a dynamic route for showing details of a particular post by adding postId as path parameter to the URL.

Add this route configuration to RouteProvider.

<RouterProvider router={route}></RouterProvider>

Components used

Navigation Menu, Home and ErrorPage components remain as shown in this post- Data Loader in React Router.

src\components\routes\PostLayout.js

import { Outlet } from "react-router";
const PostLayout = () => {
  return(
    <div className="mx-2">
      <Outlet />
    </div>
  )
}

export default PostLayout;

There is just a <Outlet /> in this component to render the content for the child route components.

src\components\routes\PostList.js

import { Link, useLoaderData } from "react-router";

const PostList = () => {
  const postData = useLoaderData();
  return(
    <>            
      <h2 className="text-info-emphasis text-center">Posts</h2>
      <ul className="list-group">
        {postData.map((post) =>
        <li className="list-group-item" key={post.id}>
            {post.id} <Link to={post.id.toString()}>{post.title}</Link>  
        </li>
        )}
      </ul>
    </>
  )
}

export default PostList


export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
    // use Response object
    throw new Response("Failed to load data", { status: response.status });
  }else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written.
  2. Uses fetch API to get data from the given URL. Only 10 posts are fetched.
  3. If there is no error in fetching data (response is ok) then you can return data directly with React Router V7.
  4. Data fetched by the loader function is retrieved by using the useLoaderData() hook.
  5. Post data is then iterated to print id and title of each post. With each post a Link with its id is also created

src\components\routes\PostDetails.js

import { useLoaderData } from "react-router";

const PostDetails = () => {
  const postData = useLoaderData();
  return(
    <>
      <h2 className="text-center">{postData.title}</h2>
      <div>{postData.body}</div>
    </>
  )
}

export default PostDetails;

export async function loader({request, params}){
  const url = "https://jsonplaceholder.typicode.com/posts/"+params.postId
  const response = await fetch(url);
  if (!response.ok) {
    throw new Response('Error while fetching post data', {status:404});
  }
  else{
    const responseData = await response.json();
    return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written which is used to fetch data by post Id.
  2. In loader function you can’t use useParams() hook to get the route parameters. Router passes an object to the loader function that has two properties- request and params.
  3. Using request you can access the request body, request URL etc. Using params you can access the route path parameters.
  4. Data fetched by the loader function is retrieved by using the useLoaderData() hook. In this case data is post data for a particular ID.
  5. Post title and post body are rendered by this component.

PostList Page

Data Loader With Dynamic Routes

Post Detail page

Loader in React Router

That's all for this topic Data Loader With Dynamic Routes in React. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Routing in React With Example
  2. Setting 404 Error Page With React Router
  3. useNavigate in React Router to Navigate Programmatically
  4. useSearchParams in React Router - Handling Query Parameters
  5. Search Filter Using useSearchParams in React Router

You may also like-

  1. React Declarative Approach
  2. React Virtual DOM
  3. Controlled and Uncontrolled Components in React
  4. JavaScript Array map() Method With Examples
  5. ArrayList in Java With Examples
  6. Count Number of Times Each Character Appears in a String Java Program
  7. Java Stream API Interview Questions And Answers
  8. What is Client Side Routing in Angular

Data Loader in React Router

In this post we'll see how to use data loader in React Router to provide data to route components.

Loader in React Router

Loader is a function that is used to fetch data before rendering a route component. That is different from the way useEffect() hook works which also provides a way to fetch data by making API calls. The useEffect() in react is called, after component has rendered, as a side effect.

With data loading in React Router, loader function is called for fetching data first then the route component is rendered. That ensures availability of the data when the component renders.

Data Loading React Router Example

In this example we'll use a loader function to fetch data from API. For API call https://jsonplaceholder.typicode.com/posts resource is used which is a free fake API. In the example Bootstrap 5 is used for styling.

Components used

There is a navigation menu.

src\components\routes\NavigationNavLink.js

import { NavLink, Outlet } from "react-router";
import "./navigation.css";
const NavigationNavLink = () => {
  const style = (({ isActive, isPending }) => 
      isPending ? "pending" : isActive ? "active" : ""
  );
  return(
    <>
      <nav id="menu" className="navbar navbar-expand-lg bg-dark navbar-dark">
        <div className="container-fluid">
          <button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
              <span className="navbar-toggler-icon"></span>
          </button>

          <div className="collapse navbar-collapse" id="navbarMenu">
            <ul className="navbar-nav">
              <li>
                  <NavLink className={style} to="/">
                      Home
                  </NavLink>
              </li>
              <li>
                  <NavLink className={style} to="/post">
                      Post
                  </NavLink>
              </li>
            </ul>
          </div>
        </div>
      </nav>
      <div className="container row mt-2">
         <Outlet />
      </div>
    </>
  );
}

export default NavigationNavLink;

CSS used for styling the active link.

src\components\routes\navigation.css

#menu a:link,
#menu a:visited {
    color: gray;
}
#menu a:hover {
    color: white;
}
#menu a.active {
    color: #7FFFD4;
}

#menu a {
    text-decoration: none;
}

#menu ul {
    gap: 1rem;
}

Home component

src\components\routes\Home.js

const Home = () => {
    return (
    <>
        <h2>This is home page</h2>
    </>
    )
}

export default Home;

PostList component

This is the component which shows the fetched posts, loader function is also written in the same component.

src\components\routes\PostList.js

import { useLoaderData } from "react-router";

const PostList = () => {
  const postData = useLoaderData();
  return(
    <div className="container">
      <h2 className="text-info-emphasis text-center">Posts</h2>
      <ul className="list-group">
        {postData.map((post) => 
        <li className="list-group-item" key={post.id}>
            {post.id} {post.title}
        </li>
        )}
      </ul>
    
    </div>
  )
}

export default PostList

export async function loader(){
  const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
  // check for any error
  if(!response.ok){
      // use Response object
      throw new Response("Failed to load data", { status: response.status });
  }else{
      const responseData = await response.json();
      return responseData;
  }
}

Some of the points to note here-

  1. In the component, loader function named loader is written.
  2. Uses fetch API to get data from the given URL. Only 10 posts are fetched.
  3. If there is no error in fetching data (response is ok) then you can return data directly with React Router V7. You don’t need to wrap in json() function like this.
    return json(await response.json());
    
  4. Other points are explained later in the post.

How to set loader in route

In the PostList component, loader function is defined but you have to let the router know that the specific loader function has to be called for the given route component. That is done using loader property in the router configuration.

src\components\routes\route.js

import { createBrowserRouter } from "react-router";
import Home from "./home";
import NavigationNavLink from "./NavigationNavLink";
import PostList, {loader as PostLoader} from "./PostList";

export const route = createBrowserRouter([
    {path: "/", element: <NavigationNavLink />,
     children: [
        {index: true, element: <Home /> },
        {path: "post", element: <PostList />, loader: PostLoader},
     ]
    },
])
As you can see loader function is imported as PostLoader
import PostList, {loader as PostLoader} from "./PostList";

and later provided in the route definition as a value of the loader property.

{path: "post", element: <PostList />, loader: PostLoader}

How to get loader data in components

Another question that arises is; how to access the data, fetched by the loader, in the component.

To access data in a component useLoaderData() hook is used which returns data from the closest route loader function.

That is how fetched post data is made available to the PostList component.

const postData = useLoaderData();

Which is then iterated to print id and title of each post.

Error handling in loaders

We should also take into consideration the scenario when loader is not able to fetch data because of some problem.

Couple of things we need to do here is to set the error message and status and also configure an error page to display the error.

When an error occurs during data fetching within a loader function, you can throw a Response object. With Response object you can include both status code and a message.

In the PostList component showed above Response object is already thrown.

if(!response.ok){
  // use Response object
  throw new Response("Failed to load data", { status: response.status });
}

Error page component

You can create a separate component for displaying errors. To get the error message and status you can use useRouteError() hook.

src\components\routes\ErrorPage.js

import { isRouteErrorResponse, useRouteError } from "react-router";

const ErrorPage = () => {
  const error = useRouteError();
  //console.log(error);
  if (isRouteErrorResponse(error)) {
    return (
      <>
        <h1>
          {error.status} {error.statusText}
        </h1>
        <p>{error.data}</p>
      </>
    );
  } else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <p>The stack trace is:</p>
        <pre>{error.stack}</pre>
      </div>
    );
  } else {
    return <h1>Unknown Error</h1>;
  }
}

export default ErrorPage;

You also need to set the Error page using the errorElement prop in the parent route. Even if an error occurs in the Child component, it will bubble up to the Root route where <ErrorPage /> component defined in the errorElement prop will catch that error.

{path: "/", element: <NavigationNavLink />, errorElement: <ErrorPage />,

For more details about setting error page, refer this post- Setting 404 Error Page With React Router

Loading state

While the data is fetched by the loader, if you want to show a loading message for better user experience then you can use useNavigation() hook to do that.

useNavigation hook provides access to the state of navigation, which are as following-

  1. idle: No navigation is happening.
  2. loading: A navigation is in progress.
  3. submitting: A form submission is in progress.

With ReactRouter V7 you should also provide hydratefallbackelement.

Using useNavigation hook to show loading state

If you are using useNavigation hook then it should be used one level above the current route. Which is logical because component is rendered only after fetching data when you are using data loading in routes. In our example navigation menu component is the suitable choice for showing loading state.

Changes needed in the NavigationNavLink component are to import useNavigation() hook and get the current navigation.

import { useNavigation } from "react-router";

then

const navigation = useNavigation();

In current navigation you can check the state if it is “loading” then display loading state. For simplicity here a simple message is displayed.

For that instead of <Outlet />, use this

{navigation.state === "loading" ? <h2>Loading!</h2>:<Outlet />}

Then loading message is displayed while data is fetched.

Data Loader in React Router

Using hydratefallbackelement property

You should also add the hydrateFallbackElement to your router configuration, otherwise you may get warning for the same.

{path: "post", element: <PostList />, loader: PostLoader, hydrateFallbackElement: <h2>Loading...</h2>,},

With fetched data

That's all for this topic Data Loader in React Router. If you have any doubt or any suggestions to make please drop a comment. Thanks!


Related Topics

  1. Search Filter Using useSearchParams in React Router
  2. Lazy Loading Routes in React

You may also like-

  1. React Declarative Approach
  2. React Virtual DOM
  3. Controlled and Uncontrolled Components in React
  4. JavaScript Array map() Method With Examples
  5. ArrayList in Java With Examples
  6. Count Number of Times Each Character Appears in a String Java Program
  7. Java Stream API Interview Questions And Answers
  8. What is Client Side Routing in Angular