Editor’s note: This article was last updated on 18 August 2023 to include the addition of newer React Router Hooks, such as useLoaderData
, useActionData
, useRouteLoaderData
, and more.
React Hooks were introduced with the release of React v16.8.0 to much excitement. With Hooks, developers can write cleaner components with less boilerplate code in comparison to class components. Many popular React packages are supporting Hooks so developers can leverage their APIs in functional components.
React Router, the most popular routing solution for React apps, added Hooks support in its v5.0 release. Recently, the library has added even newer hooks and components, increasing its features. These React Router Hooks provide developers with new ways to handle the router state, among other essential use cases.
In this tutorial, you’ll learn how to use the some of the most important Hooks with React Router and minimize the number of code lines in a component. Let’s get started!
To demonstrate how Hooks work, we’ll create a React project and set up the pages. Run the following command to create a new React app using Vite:
npm create vite@latest router-hooks-demo -- --template react
router-hooks-demo
is the app’s name, but you can name it whatever you want. Next, add the react-router-dom
package:
npm i react-router-dom --save
Import createBrowserRouter
, createRoutesFromComponents
, RouterProvider
, and Route
from the react-router-dom
package. We’ll use these components to configure client-side routing in our React app. Also, for now, we’ll only have two routes or pages: the Home
page and the User
page:
import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, createRoutesFromElements, RouterProvider, Route, } from "react-router-dom"; import App from "./App.jsx"; const Home = () => { return <div>This is the home page</div>; }; const User = () => { return <div>This is the user page</div>; }; const router = createBrowserRouter( createRoutesFromElements( <Route path="/" element={<App />}> <Route path="" element={<Home />} /> <Route path="user/:id" element={<User />} /> </Route> ) ); ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> );
In the App
component, create a navigation bar and add hyperlinks to the Home
and User
pages using React Router’s Link
component. Also, use the Outlet
component to specify the position of the child route components. On the webpage, the Link
component is rendered as an <a>
tag:
import { Link, Outlet } from "react-router-dom"; function App() { return ( <div> <nav> <div> <Link to="/">Home</Link> </div> <div> <Link to="/user/:id">User</Link> </div> </nav> <Outlet /> </div> ); } export default App;
Let’s say you need to access the current pathname of the URL inside of a page
component. Before Hooks, you would have to pass the page
component to the component
prop of the old Route
component. Then, the Route
component would inject with route props, match
, location
, and history
.
Although this approach works, the component becomes harder to read, and it’s difficult to understand how the props are injected when it comes to maintaining the project. The React Router authors added Hooks support so that the page
components can access history
, location
, and match
objects without having to pass the page
component as a component
prop in the Route
component:
// Old Route with component prop <Route path="/user/:id" component={User} />; // User component const User = (props) => { const location = props.location; console.log(location.pathname); return <div>This is the user page</div>; };
Now that you understand why we add Hooks for routing, let’s see the Hooks in action.
useParams
The useParams
Hook returns an object containing key-value pairs of any parameters that exist in a URL. For example, let’s say you have a User
page component that accepts an id
as a parameter in the URL. You can access the parameter using the useParams
Hook:
import { useParams } from "react-router-dom"; const User = () => { const params = useParams(); console.log(params); return ( // ... ) }
If you were to pass 234
as the ID in the user URL, /user/234
, the useParams()
Hook would return the following object:
{ id: 234, }
You can display the id
parameter on the page or use it to fetch the user’s information from the server:
import { useParams } from "react-router-dom"; // /user/:id const User = () => { const params = useParams(); return ( <div> <p>This is the user page</p> <p>current user Id - {params.id}</p> </div> ); };
useNavigate
In React Router v6, the useHistory
Hook was discontinued, providing useNavigate
as its alternative. The main reason for the switch from useHistory
to useNavigate
is to provide better compatibility with React Suspense.
The useNavigate
Hook returns a function that lets you handle route changes and navigate programmatically:
import { useNavigate, useParams } from "react-router-dom"; const User = () => { let navigate = useNavigate(); const params = useParams(); const handleBack = () => { navigate(-1); }; const handleNavigation = () => { navigate("/user/5"); }; return ( <div> <div>This is the user page</div> <div>current user Id - {params.id}</div> <div> <button onClick={handleBack}>Go Back</button> </div> <div> <button onClick={handleNavigation}>Go To Different User</button> </div> </div> ); };
In the code block above, we add two buttons to the User
page component. One button takes the user to the previous page, and another navigates the user to a different page. The navigate(-1)
method is equivalent to clicking on the back button. It navigates the user to the previous page and goes back by one entry in the history stack.
useLocation
The useLocation
React Router Hook allows you to access the location
object that represents the active URL. The value of the location
object changes whenever the user navigates to a new URL. The useLocation
Hook can be convenient when you have to trigger any event whenever the URL changes.
Consider that you have to keep track of views on users’ profile pages. You can detect changes in the location
object using the useEffect
Hook, which comes with React:
import { useNavigate, useParams, useLocation } from "react-router-dom"; const User = () => { const navigate = useNavigate(); const params = useParams(); const location = useLocation(); useEffect(() => { console.log(location.pathname); // Send request to your server to increment page view count }, [location]); const handleBack = () => { navigate(-1); }; const handleNavigation = () => { navigate("/user/5"); }; return ( <div> <div>This is the user page</div> <div>current user Id - {params.id}</div> <div> <button onClick={handleBack}>Go Back</button> </div> <div> <button onClick={handleNavigation}>Go To Different User</button> </div> </div> ); };
useMatch
React Router v6 has discontinued the useRouteMatch
Hook, providing the useMatch
Hook as its replacement. useMatch
returns the match data about a route relative to the current route. It matches the active URL with a given path, similar to how the Route
component works.
What problem does the useMatch
Hook solve? Say you had a /blog
route where you display multiple blog posts and a blog/:id
route where you display the details of the individual blog posts:
const App = () => { const [ posts, setPosts ] = useState([...]); const router = createBrowserRouter( createRoutesFromElements( <Route> <Route path="/blog/:id" element={<Post posts={posts} />} /> <Route path="/blog" element={<Posts posts={posts} />} /> <Route path="/" element={<Home />} /> </Route> ) ); return <RouterProvider router={router} />; } export default App;
Previously, you would have to pass all the posts to the Post
component and filter out the specific post using the useParams
Hook:
const Post = ({ posts }) => { const id = useParams().id const post = posts.find(post => post.id === Number(id)) return ( <div> <span>{post.user}</span> <p>{post.content}</p> </div> ) }
While this approach works, we can leverage the useMatch
Hook to create a cleaner implementation. The Post
component receives only the data it should display, and not the array of posts.
Use the useMatch
Hook in the app
component:
import { useMatch } from "react-router-dom"; const App = () => { const [posts, setPosts] = useState([...]); const match = useMatch("/blog/:id"); const post = match ? posts.find((post) => post.id === Number(match.params.id)) : null; const router = createBrowserRouter( createRoutesFromElements( <Route> <Route path="/blog/:id" element={<Post post={post} />} /> <Route path="/blog" element={<Posts posts={posts} />} /> <Route path="/" element={<Home />} /> </Route> ) ); return <RouterProvider router={router} />; }; export default App;
With this implementation, every time the URL changes, the command below is executed to check if the current route matches the desired URL:
const match = useMatch('/posts/:id')
If the URL matches, the match
variable will return an object containing data about the matching URL. You can then access the id
parameter from the object, use it to filter out the correct post data, and pass it to the Post
component:
const Post = ({ post }) => { return ( <div> <span>{post.user}</span> <p>{post.content}</p> </div> ) }
useNavigation
The React Router team added the useNavigation
Hook in its v6.4 release. This Hook gives the developer access to properties that show the state of a currently rendered route. For example, this Hook can indicate when a route is “loading” or when a form on the route is “submitting.” It is useful for building loading indicators or optimistically updating data on a page.
This Hook exposes some other properties, such as the location a page would navigate to after submission of a form, the JSON value of a submitted form, the text value of a submitted form, and many more.
The example below is a component with a button that disables when it is submitting a form:
import { useNavigation } from "react-router-dom"; const Home = () => { const navigation = useNavigation(); const isDisabled = navigation.state === "submitting" ? true : false; return ( <button type="submit" disabled={isDisabled}> Click Me! </button> ); };
From the example, whenever the navigation state changes to submitting
, the user cannot click the button until the form is done submitting.
useLoaderData
To understand the useLoaderData
Hook, you must understand what a loader
in React Router is. The library recently introduced this concept, and it works together with the useLoaderData
Hook. A loader is a function that returns data necessary for a route to render properly. Below is a page named Posts
with a loader named postsLoader
:
import React from "react"; export async function postsLoader() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const posts = await response.json(); return { posts }; } function Posts() { return ( <div> <h1>Posts</h1> // Extra Information </div> ); }
From the code example, the postsLoader
is an async function that returns posts. Without these posts, there would be nothing to render inside the Posts
component. Instead of fetching data using the React useEffect
Hook, React Router presents this new alternative way of doing it.
After defining a loader, add it to a route using a loader
prop:
import Posts, { postsLoader } from "./Posts.jsx"; const router = createBrowserRouter( createRoutesFromElements( <Route path="/" element={<App />}> <Route path="" element={<Home />} /> <Route path="post" element={<Posts />} loader={postsLoader} /> <Route path="user/:id" element={<User />} /> </Route> ) );
We use the useLoaderData
Hook in order to grab the loader values. For example, in our Posts
component, we will use the Hook to grab all the posts and display them:
import React from "react"; import { useLoaderData } from "react-router-dom"; export async function postsLoader() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const posts = await response.json(); return { posts }; } function Posts() { const { posts } = useLoaderData(); return ( <div> <h1>Posts</h1> {posts.map((post, index) => ( <div key={index}> <h2>{post.title}</h2> <p>{post.body}</p> </div> ))} </div> ); } export default Posts;
useActionData
In addition to loaders, React Router also added Route actions. A Route action is a function that can return data submitted to a route. This data is one submitted through different form methods such as post, put, patch, or delete.
The example below illustrates a component that submits a form, creating a new post:
import { Form } from "react-router-dom"; function NewPost() { return ( <Form method="post" action="/events"> <input type="text" name="title" /> <input type="text" name="description" /> <button type="submit">Create</button> </Form> ); } export default NewPost;
Create an action in the receiving component to have access to the submitted data:
// Imports and Loader export async function postAction({ params, request }) { const data = await request.formData(); const title = data.get("title"); const description = data.get("description"); return { title, description }; } function Posts() { // Posts Component } export default Posts;
After that, you can add the action for the route using the action
prop for a <Route />
component:
<Route path="post" element={<Posts />} loader={postsLoader} action={postAction} />
Finally, the useActionData
Hook comes in by allowing a developer to grab the values action
returned:
function Posts() { const { posts } = useLoaderData(); const { title, description } = useActionData(); return ( // Component to return ); }
useRouteLoaderData
useRouteLoaderData
is an interesting Hook that makes the loader of a currently rendered route available to any other route that needs it. The routes that need them could be children or parent routes.
The first step to take is to define an id
for a route that has a loader:
<Route path="/" element={<App />} loader={appLoader} id="app"> // Nested routes <Route path="component" element={<Component />} /> </Route>
Then, use the loader in a separate component:
import { useRouteLoaderData } from "react-router-dom"; function Component() { const data = useRouteLoaderData("app"); return ( // elements to return ); } export default NewPost;
Note that this Hook only returns a loader of currently rendered routes. If a route is not currently rendered, the Hook will return undefined
.
useBeforeUnload
The useBeforeUnload
Hook is useful for defining instructions to run before a route or element leaves the screen. Such operations may include saving an application state to localStorage
and any other use case.
Below is an illustration of useBeforeUnload
in action. This example saves form data to localStorage
whenever the user navigates off the webpage:
import { useEffect } from "react"; import { useBeforeUnload } from "react-router-dom"; function NewPost() { const [formData, setFormData] = useState({ title: "", description: "", }); useBeforeUnload(() => { localStorage.formData = JSON.stringify(formData); }); return ( // JSX Elements ); } export default NewPost;
useSubmit
The useSubmit
Hook allows a developer to manually submit a form instead of using a <Form>
component. This Hook gives the programmer more control and allows them to perform submit operations anywhere in the code.
The code example below uses useSubmit
to submit a form whenever a user changes the search value:
import { useState, useEffect } from "react"; import { Form, useSubmit } from "react-router-dom"; function Search() { const [searchInput, setSearchInput] = useState(''); const submit = useSubmit(); useEffect(() => { submit(searchInput, { method: "get", action="/search"}); }, [searchInput]); function handleInputChange(event) { const { value } = event.target; setSearchInput(value); } return ( <Form method="get" action="/search"> <input type="search" name="Search" value={searchInput} onChange={handleInputChange} /> <button type="submit">Search</button> </Form> ); } export default Search;
From the example above, we have a component called Search
that has an input bar. Due to the React useEffect
and useSubmit
Hooks, we’re able to submit a form whenever the user makes a change in the search bar, even without clicking the submit button.
Hooks are a great addition to the React ecosystem. In this article, we reviewed the code for handling routing with Hooks, using the useParams
, useNavigate
, useLocation
, and useMatch
Hooks. We also reviewed newer Hooks such as useNavigation
, useLoaderData
, useActionData
, useRouteLoaderData
, useBeforeUnload
, and useSubmit
.
Note that React Router offers many more Hooks and features React Router has — we only reviewed the most commonly used ones. But they should give you a good idea of how the other features work. Now, you’re ready to take full advantage of what React Router has to offer.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowFix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
The Angular tree view can be hard to get right, but once you understand it, it can be quite a powerful visual representation.
In this post, we’ll compare Babel and SWC based on setup, execution, and speed.
2 Replies to "Using Hooks with React Router"
I was following this as a codealong, start is a bit confusing maybe add that the changes are made in App.js.
But I get a compile error at the useEffect() part, until I realized I still had to import it from react.
I’m just 2 weeks in coding in React, so guess for a beginner they will need some more guidance
Did they change something in the ‘react-router-dom’ npm? because it says Switch is not exported from ‘react-router-dom’