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-
- In the component, loader function named loader is written.
- Uses fetch API to get data from the given URL. Only 10 posts are fetched.
- Data fetched by the loader function is retrieved using the useLoaderData() hook.
- 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.
- 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-
- In the component, loader function named loader is written which is used to fetch data by post Id.
- 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.
- Using request you can access the request body, request URL etc. Using params you can access the route path parameters.
- Data fetched by the loader function is retrieved by using the useLoaderData() hook. In this case data is post data for a particular ID.
- Post title and post body are rendered by this component.
- 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.
- 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
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-
- Action function receives an object that has properties like request, params.
- Request object has a method formData() that returns form data.
- Using params you can access the route path parameters.
- In deleteAction function postID is extracted from the URL using params and added to the URL which is sent to delete the specific post.
- Fetch API is used to make HTTP requests.
- 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.
- URL also is created conditionally, in case of edit postID has to be added as path parameter
- 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.
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-
- 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”.
- 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.
- 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.
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
You may also like-
No comments:
Post a Comment