Tuesday, June 17, 2025

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

No comments:

Post a Comment