Skip to content

learn-co-curriculum/react-router-v6-nested-routes-code-along

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nested Routing Code-Along

Learning Goals

  • Create nested routes using children and Outlet.
  • DRY up code with nested routing.
  • Pass data to nested route components using useOutletContext.

Introduction

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!

Adding a Parent Component

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 called loaders, which allow you to request data for a page as it loads. This is an incredibly powerful and useful feature of react-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.

Rendering Nested Routes as "children"

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.

Using react-router-dom's Outlet Component

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!

Practice

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.)

Passing Data via useOutletContext

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!

Accessing Outlet Context Within Child Components

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.

useOutletContext and 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 of main because UserProfile is now being rendered as a child of Home, and Home already has a main element. HTML best practices dictate that there should be only one main element per page view. And, since UserProfile only appears on a nested route, we're displaying it in an aside, as it will appear alongside the list of users we're rendering.

Conclusion

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.

Resources

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •