Skip to content

Routing - `Outlet` and `_index.tsx`

At the moment, we have only been working on one page in our app, but of course we want to add more.

So far, we’ve been using the root.tsx file to render what appears to be a single page. But looks can be deceiving: this little file is very powerful!

As you’ll see, the root.tsx file is actually the springboard from which we can render a shared layout that all of our other pages depend on.

In this step, we will:

  • Learn how to use the Outlet component to render child routes inside a parent route
  • Create a new route file in Remix
  • Understand how the file naming conventions in Remix map to the URL paths in the browser
  • Add new routes to our app and see how they are served from the correct URL paths
  • Complete a challenge to add multiple new routes to our app

Let’s start by swapping out everything between the <main> tags in our root.tsx file with a special component that Remix supplies for us: the Outlet component.

  1. Add the Outlet components as an extra import at the top of the root.tsx file:

    app/root.tsx
    import { type LinksFunction } from '@remix-run/node'
    import { Outlet, useLoaderData } from '@remix-run/react'
    import { ParallaxProvider } from 'react-scroll-parallax'
    8 collapsed lines
    import Document from '~/components/shared-layout/Document'
    import ThemeSwitch from '~/components/shared-layout/ThemeSwitch'
    import { useNonce } from '~/utils/nonce-provider.ts'
    import rootLinkElements from '~/utils/providers/rootLinkElements'
    import { type loader } from './__root.server'
    import FooterMenuRight from './components/organisms/Footer/FooterMenuRight'
    import HeaderWithSearch from './components/organisms/HeaderWithSearch'
    import useTheme from './hooks/useTheme.tsx'
  2. Comment out the all your code that is currently wrapped between the div with a className of flex-1.

    This the opening and closing <main> tag, along with all its children.

    I’ve highlighted this code in white below - your actual code will likely look different here, as you have started to customise it.

    app/root.tsx
    17 collapsed lines
    import { type LinksFunction } from '@remix-run/node'
    import { Outlet, useLoaderData } from '@remix-run/react'
    import { ParallaxProvider } from 'react-scroll-parallax'
    import Document from '~/components/shared-layout/Document'
    import ThemeSwitch from '~/components/shared-layout/ThemeSwitch'
    import { useNonce } from '~/utils/nonce-provider.ts'
    import rootLinkElements from '~/utils/providers/rootLinkElements'
    import { type loader } from './__root.server'
    import FooterMenuRight from './components/organisms/Footer/FooterMenuRight'
    import HeaderWithSearch from './components/organisms/HeaderWithSearch'
    import useTheme from './hooks/useTheme.tsx'
    export const links: LinksFunction = () => {
    return rootLinkElements
    }
    export { headers, meta } from './__root.client.tsx'
    export { action, loader } from './__root.server.tsx'
    export default function App() {
    const data = useLoaderData<typeof loader>()
    const nonce = useNonce()
    const theme = useTheme()
    return (
    <ParallaxProvider>
    <Document nonce={nonce} theme={theme}>
    <div className="flex h-screen flex-col justify-between">
    <HeaderWithSearch />
    <div className="flex-1">
    {/* <main className="h-full">
    <ParallaxBackground
    image={heroImage}
    title="Epic News"
    logo={logo}
    altText="Welcome to Epic News, where the latest developments in tech are found."
    >
    <div className="mx-auto flex w-fit flex-1 flex-col justify-between gap-16 bg-secondary/40 px-28 py-16 backdrop-blur-sm">
    <p className="text-center text-4xl font-extrabold text-secondary-foreground">
    The latest tech news in one place
    </p>
    <div className="flex justify-center gap-8">
    <Button variant="default" size="wide">
    <Link to="/signup">Sign up</Link>
    </Button>
    <Button variant="secondary" size="wide">
    <Link to="/login">Login</Link>
    </Button>
    </div>
    </div>
    </ParallaxBackground>
    </main> */}
    </div>
    7 collapsed lines
    <div className="container flex justify-between pb-5">
    <ThemeSwitch userPreference={data.requestInfo.userPrefs.theme} />
    </div>
    <FooterMenuRight />
    </div>
    </Document>
    </ParallaxProvider>
    )
    }
  3. Add the Outlet component beneath the commented out code

    app/root.tsx
    17 collapsed lines
    import { type LinksFunction } from '@remix-run/node'
    import { Outlet, useLoaderData } from '@remix-run/react'
    import { ParallaxProvider } from 'react-scroll-parallax'
    import Document from '~/components/shared-layout/Document'
    import ThemeSwitch from '~/components/shared-layout/ThemeSwitch'
    import { useNonce } from '~/utils/nonce-provider.ts'
    import rootLinkElements from '~/utils/providers/rootLinkElements'
    import { type loader } from './__root.server'
    import FooterMenuRight from './components/organisms/Footer/FooterMenuRight'
    import HeaderWithSearch from './components/organisms/HeaderWithSearch'
    import useTheme from './hooks/useTheme.tsx'
    export const links: LinksFunction = () => {
    return rootLinkElements
    }
    export { headers, meta } from './__root.client.tsx'
    export { action, loader } from './__root.server.tsx'
    export default function App() {
    const data = useLoaderData<typeof loader>()
    const nonce = useNonce()
    const theme = useTheme()
    return (
    <ParallaxProvider>
    <Document nonce={nonce} theme={theme}>
    <div className="flex h-screen flex-col justify-between">
    <HeaderWithSearch />
    <div className="flex-1">
    {/* <main className="h-full">
    20 collapsed lines
    <ParallaxBackground
    image={heroImage}
    title="Epic News"
    logo={logo}
    altText="Welcome to Epic News, where the latest developments in tech are found."
    >
    <div className="mx-auto flex w-fit flex-1 flex-col justify-between gap-16 bg-secondary/40 px-28 py-16 backdrop-blur-sm">
    <p className="text-center text-4xl font-extrabold text-secondary-foreground">
    The latest tech news in one place
    </p>
    <div className="flex justify-center gap-8">
    <Button variant="default" size="wide">
    <Link to="/signup">Sign up</Link>
    </Button>
    <Button variant="secondary" size="wide">
    <Link to="/login">Login</Link>
    </Button>
    </div>
    </div>
    </ParallaxBackground>
    </main> */}
    <Outlet />
    </div>
    7 collapsed lines
    <div className="container flex justify-between pb-5">
    <ThemeSwitch userPreference={data.requestInfo.userPrefs.theme} />
    </div>
    <FooterMenuRight />
    </div>
    </Document>
    </ParallaxProvider>
    )
    }
  4. Save your changes and head back across to your browser, but make sure you are on the home page at http://localhost:3000.

    You should see the screen content has changed:

    Hello from _index.tsx screenshot

Why can we still see the navbar and footer, but the central page content has changed? And where is this new code coming from?

Well - the screenshot itself provides a big clue to this last question!

Open the file at app/routes/_index.tsx and you’ll find the matching code:

app/routes/_index.tsx
import { type MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = () => [{ title: "Epic News" }];
export default function Index() {
return (
<main className="grid h-full place-items-center">
<h1 className="text-mega">
Hello from{" "}
<pre className="prose rounded-lg bg-primary p-6 text-primary-foreground">
app/routes/_index.tsx
</pre>
</h1>
</main>
);
}

So what exactly is happening here?? Why is the Outlet component rendering the _index.tsx file?

So, how does Remix know which route files to load as “children” of “parents”? 🤔

The answer lies in Remix’s file names.

The structure of your project’s files and folders directly maps to the structure of your routes in the browser URL bar.

We can see this in action by adding a new route to our app. Let’s add a dummy ‘about us’ page.

Create a new file at app/routes/about-us.tsx:

About us page file

Inside this new file, add the following code and save your changes:

app/routes/about-us.tsx
export default function AboutUsRoute() {
return (
<main className="container py-16">
<h1 className="text-mega">About us</h1>
</main>
);
}

Head back over to your browser and navigate to http://localhost:3000/about-us. You should see the new ‘About us’ page:

About us page navigation

Notice how the navbar, footer and theme switcher are still present on the page?

Think of the Outlet component as a “window” or “placeholder” for the child route. It’s a way of telling Remix where to render the child route’s code inside the parent.

Finally, let’s update the navbar links to include the new ‘About us’ page.

Open app/components/organisms/HeaderWithSearch.tsx and add the following code to the navbar section:

app/components/organisms/HeaderWithSearch.tsx
export default function HeaderWithSearch() {
const matches = useMatches()
const isOnSearchPage = matches.find(m => m.id === 'routes/users+/index')
const searchBar = isOnSearchPage ? null : <SearchBar status="idle" />
return (
<header className="bg-primary py-6">
<nav className="container flex flex-wrap items-center justify-between gap-4 sm:flex-nowrap md:gap-8">
<Link to="/">
<div className="flex items-center gap-4">
<img src={logo} alt="Epic News Logo" className="w-16" />
<span className="text-sm text-foreground">Epic News</span>
</div>
</Link>
<div className="flex flex-1 justify-center gap-8">
<Link
to="/news"
className="text-sm font-semibold text-muted-foreground transition hover:text-foreground"
>
News
</Link>
<Link
to="/about-us"
className="text-sm font-semibold text-muted-foreground transition hover:text-foreground"
>
About us
</Link>
</div>
<div className="ml-auto hidden max-w-sm flex-1 sm:block">
{searchBar}
</div>
<div className="flex items-center gap-10">
<LoginOrUserDropdown />
</div>
<div className="block w-full sm:hidden">{searchBar}</div>
</nav>
</header>
)
}

We can further speed up page loading times by adding a special prefetch attribute to Remix’s Link component.

When a user hovers over a link with the prefetch attribute, Remix will begin to load the linked page in the background. This means that when the user clicks on the link, the page will load almost instantly.

Add the prefetch attribute to the new ‘About us’ link in the navbar:

app/components/organisms/HeaderWithSearch.tsx
<Link
to="/about-us"
prefetch="intent"
className="text-sm font-semibold text-foreground transition hover:text-primary-foreground"
>
About us
</Link>

You can see this behaviour in action if you open devtools in your browser, navigate to the ‘Network’ tab, and hover over the ‘About us’ link in the navbar:

Link prefetching in action

Notice how the browser begins to load the ‘about-us’ page in the background as soon as you hover over the link? By the time you click on the link, the page will load almost instantly.

In this tutorial we have:

  • Learned how to use the Outlet component to render child routes inside a parent route
  • Discovered how to create a new route file in Remix
  • Understood how the file naming conventions in Remix map to the URL paths in the browser
  • Added new routes to our app and seen how they are served from the correct URL paths

In the next lesson, we will learn how to create sibling routes with their own children.

This will allow us to create a more complex route structure that can serve multiple pages from the same level in the app.