Routing - `Outlet` and `_index.tsx`
Introduction
Section titled “Introduction”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
The Outlet
component
Section titled “The Outlet component”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.
-
Add the
Outlet
components as an extra import at the top of theroot.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 linesimport 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' -
Comment out the all your code that is currently wrapped between the
div
with aclassName
offlex-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 linesimport { 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"><ParallaxBackgroundimage={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>)} -
Add the
Outlet
component beneath the commented out codeapp/root.tsx 17 collapsed linesimport { 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<ParallaxBackgroundimage={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>)} -
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:
Wait - what just happened? 🤔
Section titled “Wait - what just happened? 🤔”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:
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?
What is a “route”? 🔀
Section titled “What is a “route”? 🔀”In a web app, a “route” is a web address (URL) that shows a specific page or view in the app. Every route is served from a specific file in the code base.
For example, when we run a Remix application on our local machines, the base URL is
http://localhost:3000
In a Remix app, this is initially served from the file at app/root.tsx
.
However, if we wanted to navigate to a different page - say the ‘About Us’ page - then we would typically add a new path onto the end of the base URL like so:
http://localhost:3000/about-us
The “route” in this case would then be served from another file in the code base.
In a Remix app, this file would be placed inside the routes
folder, and called about-us.tsx
.
Directoryapp
- root.tsx
Directoryroutes
- _index.tsx
- about-us.tsx
The file name - about-us.tsx
- and the URL path - /about-us
- must match in order for Remix to serve the correct page.
Parent and child routes
Section titled “Parent and child routes”A “child” route is nested within another route, which we call a “parent”.
As soon as we place the Outlet
component inside a route file (like we just have in root.tsx
), it converts the file into a “parent” route.
Outlet
tells Remix where to render the matching child route(s) within the parent.
Because the parent is already loaded into memory, this makes page loads quicker because Remix only needs to load the new content from the child.
This is really fast! 🚀🚀🚀
The main index page of a website
Section titled “The main index page of a website”Hover your cursor over the two buttons below to see how the parent root.tsx
renders its child code through the <Outlet />
component:
Hello from app/routes/_index.tsx
The route structure
Section titled “The route structure”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.
Adding a new route
Section titled “Adding a new route”Create a new file at app/routes/about-us.tsx
:
Route file names in Remix
Section titled “Route file names in Remix”Take extra care when naming your route files in Remix. The file name must match the URL path that you want to serve from it exactly.
For example, because we want to serve a page from the URL path /about-us
, then the file name must also be about-us.tsx
.
If it was named aboutUs.tsx
or about_us.tsx
, then Remix would not be able to serve the correct page from the URL.
Inside this new file, add the following code and save your changes:
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:
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.
Update navbar links
Section titled “Update navbar links”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:
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> )}
Link prefetching
Section titled “Link prefetching”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:
<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:
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.
Challenge
Section titled “Challenge”Add a new /contact-us
route to your app. Below are the requirements:
- For the time being, this page should just show a simple title, just like
/about-us
. - You should also add a link to the new page in your navbar
- Add the
prefetch
attribute to the new navbar link to speed up page loading times.
Visit each page in your browser to check that they are being served correctly:
- Visiting
http://localhost:3000/contact-us
should show a page with the title “Contact us” - Make sure the navbar and footer still show as expected on each page.
Summary
Section titled “Summary”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
📄 Assignment documentation
Section titled “📄 Assignment documentation”We need to update your assignment to document the new features added in this section. 🚀
- Explain what the
Outlet
component is and how it is used in a Remix application. - Describe how the file naming conventions in Remix map to the URL paths in the browser.
- Explain how to add new routes to a Remix application and how they are served from the correct URL paths.
- Explain why the
Link
component makes navigation between pages faster in a Remix application. - What is the
prefetch
attribute and how does it speed up page loading times even more in a Remix application?
What’s next?
Section titled “What’s next?”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.