- Create nested routes using
children
andOutlet
. - DRY up code with nested routing.
- Pass data to nested route components using
useOutletContext
.
In this code-along, we're going to keep working with our Social Media application we made in the previous code-along. However, we want to make some updates!
First of all, we don't want to have to include our NavBar
component in every
page level component — that wasn't very DRY!
Second of all, we don't want to navigate to a brand new web page to view a specific user. Instead, we want that user to display on the same page as the list of users! But we do still want each user to have their own URL so that we can share links to specific users if we want to.
Finally, we want to set up our user state in a way that allows us to easily pass data from route to route.
All of this can be done using Nested Routing. Nested Routing allows us to re-render specific pieces of a webpage when a user navigates to a new route, rather than re-rendering the entire page. This can be great for developers, as it allows easy reuse of certain components, and also for users, as it can make navigating a website smoother and easier.
To add Nested Routing to our application, we'll need to use a few other things
from react-router-dom
: the children
attribute on our route objects, the
Outlet
component, and the useOutletContext
hook. Let's dive into each of
those in turn!
In our last code-along, you might have noticed that we didn't have an App
component in our list of components. In fact, there was no single parent
component to our whole application! Instead, we just had a bunch of parallel
components, each of which was rendering on its own route.
While this parallel approach definitely works, and might be the right decision
depending on the app you're building, it has some drawbacks. As mentioned above,
we had some code that wasn't very DRY — we used the NavBar
component in every
one of our page views.
Moreover, the only way we could have declared global state for our application would have been through creating our own contextProvider with the useContext hook. While this is, once again, a perfectly reasonable approach, it can be nice to have a parent component that can instantiate and pass down global application state when your app first loads.
Note: We could have also used a more advanced feature of
react-router
calledloaders
, which allow you to request data for a page as it loads. This is an incredibly powerful and useful feature ofreact-router
, but it takes a fair bit of overhead to implement. To read more about loaders, check out the documentation.
There are many different ways to solve these problems, and the best solution will often depend on what you're trying to build. As a beginner, it's best to learn a variety of design patterns, so you can intelligently apply the right one to your own unique situation!
Okay, enough theorizing — let's get to actually creating this parent Layout
component. We will pick up where we left off with the last code-along, but note
that we've already added the Layout.jsx
file for you.
If you haven't already, go ahead and fork and clone the repo for this
code-along. Then run npm install
to install the dependencies, npm run server
to start your json-server
, and npm start
to open the application in the
browser.
react-router-dom
gives us a variety of options we can include in our route
objects; so far, we've covered path
and element
. Another
option, children
, is how we can tell a route that it has nested routes.
We'll start by setting up the Layout
component. This component is already created
in the pages/ folder, so all we need to do is include our NavBar
component directly
within our Layout
, rather than dropping it into every page-level component:
// Layout.jsx
import NavBar from "./components/NavBar"
function Layout(){
return(
<>
<header>
<NavBar />
</header>
</>
)
}
Much easier! And, if we create a new page for our website, we don't have to
remember to include the NavBar
component within that new page.
Remember to remove the header
containing the NavBar
from the About
,
Login
, ErrorPage
, and Home
components after adding this code to Layout
.
Next, go ahead and update our routes in App.jsx
to include the following code.
This will render each of our page-level components as a nested route of our /
path and our Layout
component:
// App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom"
import Layout from "./pages/Layout"
import Home from "./pages/Home"
import About from "./pages/About"
import Login from "./pages/Login"
import UserProfile from "./pages/UserProfile"
import ErrorPage from "./pages/ErrorPage"
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={Layout}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
<Route path="/profile/:id" element={<UserProfile />} />
<Route path="*" element={<ErrorPage />}/>
</Route>
</Routes>
</BrowserRouter>
)
}
export default App
First, we imported the Layout
component and added it as the parent component in
our routes array.
Second, by entering our different route objects as an array associated with our
Layout
route's children
key, we've set them up to render inside of our Layout
component. That means that if we navigate to any of these nested routes — such
as /login
, for example — our Layout
component will render with our Login
component as a child component.
Note that it's okay for our Home
component to have the same path as our parent
Layout
component. All child route paths must start with their parent's route
path, and one of them (but only one) can exactly match its parent's route
path.
If you've opened the code up in your browser, you might have noticed that our app still isn't working correctly — visiting the child routes doesn't actually render the pages we want.
That's because there is still one tool we need to implement from
react-router-dom
in order to get our nested routes up and running: the
Outlet
component.
An Outlet
component is included within a component that has nested routes. It
basically serves as a signal to that parent component that it will render
various different components as its children, depending on what route a user
visits. The Outlet
component works in conjunction with the router
to
determine which component should be rendered based on the current route.
Including it in a component is pretty straightforward:
// Layout.jsx
import { Outlet } from "react-router-dom"
import NavBar from "./components/NavBar"
function Layout(){
return(
<>
<header>
<NavBar />
</header>
<Outlet />
</>
)
}
And boom! We have nested routing!
Ok, let's try setting up another nested route. As mentioned in our introduction,
we want to view a specific user profile while still viewing the list of all our
available users. We can implement this feature by making our UserProfile
component a nested route within our Home
component.
Let's update our App.jsx
file to make that change!
// App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom"
import Layout from "./pages/Layout"
import Home from "./pages/Home"
import About from "./pages/About"
import Login from "./pages/Login"
import UserProfile from "./pages/UserProfile"
import ErrorPage from "./pages/ErrorPage"
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={Layout}>
<Route path="/" element={<Home />}>
<Route path="/profile/:id" element={<UserProfile />} />
</Route>
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<ErrorPage />}/>
</Route>
</Routes>
</BrowserRouter>
)
}
export default App
We'll need to also make sure we update our Home
component to use the Outlet
component from react-router-dom
.
// Home.jsx
import { useState, useEffect } from "react"
import { Outlet } from "react-router-dom"
import UserCard from "../components/UserCard"
function Home(){
const [users, setUsers] = useState([])
useEffect(() =>{
fetch("http://localhost:4000/users")
.then(r => {
if (!r.ok) { throw new Error("failed to fetch users") }
return r.json()
})
.then(data => setUsers(data))
.catch(error => console.error(error))
}, [])
const userList = users.map(user =>{
return <UserCard key={user.id} user={user}/>
});
return (
<main>
<h1>Home!</h1>
<Outlet />
{userList}
</main>
)
}
export default Home
Try navigating to one of our user profile routes. You should see that profile component rendering at the top of the page, above our list of users! (It won't look like much, at present, since we're only rendering a user's name. In a real app, you'll like be displaying more information and will make things look a lot snazzier using CSS.)
What if we need to pass data from a parent component to a nested route? We're
invoking the Outlet
component within the parent component instead of any of
our child components, so we can't pass props in our usual way.
The answer is to create a Context Provider using React's useContext
hook.
Fortunately react-router-dom
already has this feature built in to Outlet
components via the useOutletContext
hook!
We can pass data to our Outlet
component via a context
prop, then access it
in whatever child component needs the data using the useOutletContext
hook.
<Outlet context={data}/>
const data = useOutletContext()
If you want to pass multiple pieces of data, you can pass either an array or an
object to the context prop, then destructure it when you invoke the
useOutletContext
hook:
<Outlet context={{firstProp: firstData, secondProp: secondData}}/>
const {firstProp, secondProp} = useOutletContext()
Let's change our code such that our users
data is being fetched within our
Layout
component. We'll then want to pass users
down via our Outlet
component's context
prop, so that we can access it within our nested routes.
// Layout.jsx
import { useState, useEffect } from "react"
import { Outlet } from "react-router-dom"
import NavBar from "./components/NavBar"
function Layout(){
const [users, setUsers] = useState([])
useEffect(() =>{
fetch("http://localhost:4000/users")
.then(r => {
if (!r.ok) { throw new Error("failed to fetch users") }
return r.json()
})
.then(data => setUsers(data))
.catch(error => console.error(error))
}, [])
return(
<>
<header>
<NavBar />
</header>
<Outlet context={users}/>
</>
)
}
Now, within our Home
component we can use the useOutletContext
hook to
access that piece of data:
// Home.jsx
import { Outlet, useOutletContext } from "react-router-dom"
import UserCard from "../components/UserCard"
function Home(){
const users = useOutletContext()
const userList = users.map(user => <UserCard key={user.id} user={user}/>)
return (
<main>
<h1>Home!</h1>
<Outlet />
{userList}
</main>
)
}
export default Home
We should see our list of users rendering just as it was before!
Like with any context provider, we can actually access data that we pass to our
Outlet
component's context
prop within deeply nested components.
Take our UserCard
component, for example. We don't need our whole array of
user data in this component, but for the sake of demonstration we're going to
update this component to include the following code:
// UserCard.jsx
import { Link, useOutletContext } from "react-router-dom"
function UserCard({user}) {
const users = useOutletContext()
console.log(users)
return (
<article>
<h2>{user.name}</h2>
<p>
<Link to={`/profile/${user.id}`}>View profile</Link>
</p>
</article>
)
}
export default UserCard
We should be seeing our array of four users being logged to our browser console.
Instead of passing props from Home
to UserCard
, we can just use the
useOutletContext
hook to directly access the data that was originally passed
to our Outlet
component in Layout
. This is a very helpful feature if you ever
need to pass data to a deeply nested component, and is a great reason to use
Context Providers in general with React's useContext
hook.
However, there is a small hitch that we run into if we have deeply nested routes.
If we look at our routes in our App.jsx
file, we'll see that we have a
deeply nested route:
// App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom"
import Layout from "./pages/Layout"
import Home from "./pages/Home"
import About from "./pages/About"
import Login from "./pages/Login"
import UserProfile from "./pages/UserProfile"
import ErrorPage from "./pages/ErrorPage"
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={Layout}>
<Route path="/" element={<Home />}>
<Route path="/profile/:id" element={<UserProfile />} />
</Route>
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<ErrorPage />}/>
</Route>
</Routes>
</BrowserRouter>
)
}
export default App
Our Home
route is nested within our Layout
route, and our UserProfile
route
is nested within our Home
route.
If we provide a piece of data to the Outlet
component within our Layout
, and we
want to access it within our UserProfile
component, we'll have to pass that
data to the Outlet
component within our Home
component first.
Essentially, useOutletContext
only looks at the immediate parent Outlet
for data. So, if we have one Outlet
nested within another Outlet
, we'll need
to make sure we pass data to that inner Outlet
as well:
// Home.jsx
import { Outlet, useOutletContext } from "react-router-dom"
import UserCard from "../components/UserCard"
function Home(){
const users = useOutletContext()
const userList = users.map(user => <UserCard key={user.id} user={user}/>)
return (
<main>
<h1>Home!</h1>
<Outlet context={users}/>
{userList}
</main>
)
}
export default Home
Now we can successfully access that data within our UserProfile
component.
// UserProfile.jsx
import { useParams, useOutletContext } from "react-router-dom";
function UserProfile() {
const params = useParams()
const users = useOutletContext()
const user = users.find(user => user.id === parseInt(params.id))
return(
<aside>
{ user.name ? <h1>{user.name}</h1> : <h1>Loading...</h1> }
</aside>
)
}
export default UserProfile
We could have still used a useEffect
and a fetch
to load specific user data
as we did in the previous code along, but in this case we're finding our
specific user using our array of user state passed by useOutletContex
and the
.find
method, for the sake of demonstration.
Note: We're using an
aside
here instead ofmain
becauseUserProfile
is now being rendered as a child ofHome
, andHome
already has amain
element. HTML best practices dictate that there should be only onemain
element per page view. And, sinceUserProfile
only appears on a nested route, we're displaying it in anaside
, as it will appear alongside the list of users we're rendering.
To review, we learned how to set up Nested Routes using react-router-dom
,
which will allow us to only re-render specific portions of our webpage and
include a global parent component for our whole app.
It's not a requirement to use Nested Routing in an application, but it's an incredibly powerful tool to have at our disposal.
In the next section, we'll look at two other powerful tools, the useNavigate
hook and the Navigate
component, to learn how to add programmatic navigation
to our applications.