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 "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 { 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-
- In the component, loader function named loader is written.
- Uses fetch API to get data from the given URL. Only 10 posts are fetched.
- If there is no error in fetching data (response is ok) then you can return data directly with React Router V7.
- 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.
- 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
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-
- In the component, loader function named loader is written which is used to fetch data by post Id.
- 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>},
- Clicking on Edit button navigates to /post/POST_ID/edit path
- Delete button functionality is not given in this post.
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
You may also like-
No comments:
Post a Comment