Outlet and Outlet Context in React.js

robert bubnikrobert bubnik
9 min read

In this article, we're going to use some react router tools to take advantage of routes. They'll serve to guide the user from page to page with information displaying when and where we want it to without having to repeat ourselves. By the end, you should have a fair understanding of how Outlet and Outlet Context allow for organization of routes and their children routes, and how we pass information along to them. I built the examples below using create-react-app if you'd like to follow along.

If you've created your first react app, you probably started out with a index.js file like this:

First, we import createBrowserRouter and RouterProvider from react-router-dom.

Install react router via your terminal

 npm install react-router-dom@6
import { createBrowserRouter, RouterProvider } from "react-router-dom";

Next create a .js file that will hold your routes, such as routes.js. Leave it blank for now. Import the routes file into index.js.

we then pass our 'routes' file to the createBrowserRouter(), and store it in a variable, perhaps calling it 'router'

const router = createBrowserRouter(routes);

when we run root.render in our index.js file, we'll use <RouterProvider/> with the 'router' prop set to variable we created above.

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);

Now, lets set up our routes. Since create-react-app starts us off with an App.js file, lets import that into routes. Then,

const routes =[

    {
        path: "/",
        element: <App />
      }

]

you've now created your first route.

You can test this by renaming the 'path' to /test-test

then in your browser, after your localhost number type /test-test

Your <App/> page should render, make sure you placed some text inside the main <div>

now, lets create some children components. You can name them what you like, here I will call them Thing 1, Thing 2, etc.

In your main route object, add the key "children", with each element set the appropriate component and path name.

import App from "./App"
import Thing1 from "./Thing1"
import Thing2 from "./Thing2"

const routes =[

    {
        path: "/",
        element: <App />,
        children: [
            {
                path:'/Thing1',
                element: <Thing1/>
            },
            {
                path:'/Thing2',
                element: <Thing2/>
            }
        ]
      }

]

export default routes

what is the 'children' key in a route?

It lists all the paths and components we want nested under a specific route. Here, we are only nesting once under the "/" path. So we can refer to App at this point as the parent of Thing1 and 2. Congratulations you've got twins.

you will see that even if you go to /Thing1 or Thing2 routes in the browser, the components do not render yet. Though we've created children, they can't do much on their own, and they call to their parents for help. You will need to import another tool from react router, Outlet. Outlet it the phone line that allows parent's to speak to their children, first, by telling them to show up when their url name is called. By setting up the phone line, each of our children gets their own phone connection with a single line of code.

import { Outlet } from "react-router-dom";

Then place <Outlet/> in the App page

function App() {
  return (
    <div >
      <header >

      </header>
      <div>
        This is the app page
      </div>
<Outlet/>
    </div>
  );
}

We needed to place <Outlet/> in the App.js so that it will be aware of the children components we created inside our main route "/" or "http://localhost:3000/" if you are running your react app on port 3000.

Now we see that our page renders Thing1 or Thing2's text below our main text that is written in App.js.

Why do we want to have routes be children of our home component?

As you can see, simply by placing components as 'children' of the main route, we have avoided having to place our Thing1 or Thing2 component directly inside the App component. Another plus is that the App text remains static while our subtext of Thing1 or Thing2 is dynamic based on the route. This avoids needing to place our main text "This is the app page" inside each of the components Thing1 and 2. If we had a navigation bar for example and wanted it rendered on each child component, we've avoided having to import that navbar for each one.

Lets now give Thing 1 and 2 something else to show besides their names, and we'll have App pass that data to each one via OutletContext.

First, create some data inside App, here I have an object of Thing1 and 2's favorite foods.

import './App.css';
import { Outlet } from "react-router-dom";

function App() {

  const foods = {
    Thing1:'cake',
    Thing2: 'ice-cream'
  }

  return (
    <div >
      <header >

      </header>
      <div>
        This is the app page
      </div>
<Outlet context={foods}/>
    </div>
  );
}

export default App;

I then name a prop "context" and pass it the foods object, in <Outlet/>.

Now to have Thing 1 and 2 gather that information. Remember, Thing1 and 2 are listing on the phone line, but so far we've only told them to show up when called. Now the App parent is shouting "Context!" so the kids know what say once they appear to the page.

import the {useOutletContext} hook into Thing1 component so that it can hear that context message.

Then access the "Thing1" property and watch as it renders the favorite food next to its name.

import { useOutletContext } from "react-router-dom"

function Thing1 () {

   const favoriteFood = useOutletContext().Thing1

    return (
        <div>
        Thing1
        {favoriteFood}
    </div>
    )
}


export default Thing1

With useOutletContext, you've avoided needing to place Thing1 or 2 inside the App.js component and also needing to pass any of them props.

If we create a new path: '/all', and pass in both Thing1 and 2 inside a fragment, we can render both to the page

const routes =[

    {
        path: "/",
        element: <App />,
        children: [
            {
                path:'/Thing1',
                element: <Thing1/>
            },
            {
                path:'/Thing2',
                element: <Thing2/>
            },
            {
            path:'/all',
            element:<><Thing1/> <Thing2/></>
            }
        ]
      }

]

Or if we had an extensive list of Things, create a variable storing each in an array and .map through it.

const all = [
    <Thing1/>, <Thing2/>
]
const routes =[

    {
        path: "/",
        element: <App />,
        children: [
            {
                path:'/Thing1',
                element: <Thing1/>
            },
            {
                path:'/Thing2',
                element: <Thing2/>
            },
            {
            path:'/all',
            element: all.map(thing=> {
                return thing
            })
            }
        ]
      }

]

export default routes

Normally perhaps we would create a general "Thing" component and be .mapping through a data set we fetched from an API or have in a local database. I am only demonstrating that if we wished to create a unique component for each Dr. Seuss character and give it its own route, we could do so.

Now that we've seen the basic principles, we can image what we can do to keep our routes and component organized when we have a more complex structure to mange.

Let's zoom out a little.

Say we were showing the user not just Thing 1 and 2, but also various books from Dr. Seuss and their characters.

We now could have a parent route for all the books and have its children paths render the ExampleBook.js as the element. Similar to what the Thing1 & 2 example, I've created an object with some data at the Books.js level and we'll see how we pass down the appropriate context to each book so they can render their own characters, to keep things more organized. Keep in mind Books.js is now another child of App route "/", and each book route becoming a grandchild of App. The family grows.

import { Outlet } from "react-router-dom";

function Books () {

    const books ={
    catinthehat:{
        characters:[
            'Thing 1', 'Thing 2', 'The Cat in the Hat', 'Sally Walden', 'Fish'
        ]
    },
    greeneggs:{
        characters:[
            'Sam-I-Am', 'Guy-I-Am', 'Mouse', 'Fox' 
        ]
    },
    thelorax:{
        characters:[
            'The Lorax', 'Swomee Swans', 'Barbaloot Bears', 'Humming Fush'
        ]
    }
    }

    return (
        <div>
           <h2>Dr Seuss books</h2> 
           <Outlet context={books} />
        </div>

    )
}

export default Books

Now lets see how one of our books turned out:

import { useOutletContext } from "react-router-dom"

function CatInTheHat () {

    const characters = useOutletContext().catinthehat.characters
    return (
        <div>
        <h3>
        Cat in the Hat Characters 
        </h3>
        <ul>
        {characters.map(character => 
           <li key={character}>{character}</li>
        )}
        </ul>
        </div>
    )

}

export default CatInTheHat

Here I have the Books.js simply print Dr. Seuss Books to the screen, but we might have additional navigation bar for the user to toggle through each book. I've left out the setup of the navigation aspect for simplicity.

We've setup a simple series of routes and children routes to keep our user flowing smoothly from page to page in a cascading manner, only keep in mind that the context of a parent only passes down to it's direct children. For example the data living in App component would not be passed down to each individual book unless we passed it down to the books level first. Follow the sampleData string I've created in the foods object:


import './App.css';
import { Outlet } from "react-router-dom";

function App() {

  const foods = {
    Thing1:'cake',
    Thing2: 'ice-cream',
    sampleData:'Here is the data from the App level or \'grandparent\' '
    }



  return (
    <div >
      <header >

      </header>
      <div>
        This is the app page
      </div>
<Outlet context={foods} />
    </div>
  );
}

export default App;

Now one level lower, Books, who needs to use UseOutletContext:

import { Outlet } from "react-router-dom";
import { useOutletContext } from "react-router-dom";

function Books () {

    const sampleData = useOutletContext().sampleData

    const books = {
    catinthehat:{
        characters:[
            'Thing 1', 'Thing 2', 'The Cat in the Hat', 'Sally Walden', 'Fish'
        ]
    },
    greeneggs:{
        characters:[
            'Sam-I-Am', 'Guy-I-Am', 'Mouse', 'Fox' 
        ]
    },
    thelorax:{
        characters:[
            'The Lorax', 'Swomee Swans', 'Barbaloot Bears', 'Humming Fush'
        ]
    }
    }

    return (
        <div>
           <h2>Dr Seuss books</h2> 
           <Outlet context={{books,sampleData}} />
        </div>

    )
}

export default Books

We've used some object destructing to allow us to tell context that it has multiple objects it's going to point to. Finally, our 'grandchild' of App, CatInTheHat, will need to display the data, here I've continued to call it sampleData in a variable.

import { useOutletContext } from "react-router-dom"

function CatInTheHat () {

    const characters = useOutletContext().books.catinthehat.characters
    const sampleData = useOutletContext().sampleData

    return (
        <div>
        <h3>
        Cat in the Hat Characters 
        </h3>
        <ul>
        {characters.map(character => 
           <li key={character}>{character}</li>
        )}
        </ul>
        {sampleData}
        </div>
    )

}

export default CatInTheHat

With this, we've held and shared data at different levels of component hierarchy without requiring the children to live in the same url or "house" as their parents or grandparents. Talk about independence. This provides some precision when sharing a url path, knowing that it will point our user to the data they came for, without requiring them to take the time to navigate through our page.

I hope this article was as beneficial for you to read as it was for me to write. Go forth my children.

0
Subscribe to my newsletter

Read articles from robert bubnik directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

robert bubnik
robert bubnik