Nested Routes and Parameterized Routes In Remix

Aaron K Saunders - Feb 15 '22 - - Dev Community

Overview

This video will walk through building a Remix app that shows a list of teams and then a detailed view of each team. On the detailed page, we will have a list of players, which when clicked will show a detailed view of the player. The purpose of all of this is to show how to use Nested Routes In Remix

  • The parent view/container will hold the navigation and the child components will be rendered in the provided Outlet
  • We also show how to use the OutletContext that is provided for you by react-router

This is what the final directory structure with files will look like

Image description

Video

Source Code

Code and Descriptions

The first thing we want to do is have the index.tsx redirect to our main page. I did not know of another way to do it using the router, so I just redirect in the loading of the index page

// index.tsx
import { redirect } from "remix";

export const loader = async () => {
  return redirect("/teams");
};

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>

    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, we create the root of the team pages we will be working on within the application. When the browser I directed to render /teams we will render teams.tsx And /teams/index.tsx

// teams.tsx
import { Link, Outlet, useLocation } from "remix";

export default function Teams() {
  const teams = [
    {
      id: 1,
      name: "Team One",
      players: [
        {
          id: 1,
          name: "player one team one",
        },
        {
          id: 2,
          name: "player two team one",
        },
        {
          id: 3,
          name: "player three team one",
        },
      ],
    },
    { id: 2, name: "Team Two" },
    { id: 3, name: "Team Three" },
  ];

  // used for displaying the current application path
  const location = useLocation();

  return (
    <div
      style={{
        borderWidth: 1,
        borderColor: "grey",
        border: "solid",
        padding: 8,
      }}
    >
      <h1>TEAMS</h1>
      <Link to="/" style={{ margin: 8 }}>
        Home
      </Link>
      <Link to="create" style={{ margin: 8 }}>
        Add New Team
      </Link>
      <div style={{ margin: 8, marginTop: 32, background: "grey" }}>
        <!-- all of the pages in the /teams directory will be -->
        <!-- rendered here at this outlet, we can also pass   -->
        <!-- context information through the router           -->
        <Outlet context={[teams]} />
      </div>
      <pre>{location.pathname}</pre>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

this is the code for /teams/index.tsx. here we are just rendering a list of teams which was passed down through the context that is defined in the router. We use the hook useOutletContext to get access to the context properties.

// /teams/index.tsx
import { Link, useOutletContext } from "remix";

export default function TeamsIndex() {
  const [teams] = useOutletContext() as any;
  return (
    <div>
      <div
        style={{
          padding: 16,
          borderWidth: 1,
          borderColor: "grey",
          border: "solid",
        }}
      >
        <p>This is where the individual teams will appear</p>
        {teams?.map((t: any) => (
          <Link to={`/teams/${t.id}`}>
            <p>{t.name}</p>
          </Link>
        ))}
      </div>

    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As we loop through the teams in the array we got through the context, we want to be able to drill down, but keep the layout/framework around the TeamsIndex component. We do that by the way we structure the path for the next route.

  <Link to={`/teams/${t.id}`}>
     <p>{t.name}</p>
  </Link>
Enter fullscreen mode Exit fullscreen mode

The route /teams/<id> will be rendered in the same outlet that was defined in the /teams/index.tsx.

So now to see the detailed page, $teamId.tsx, with the team information and the list of players on the team, here is what the page would look like. The $ in front of the name of the file is called parameterized route... meaning that when the route is resolved I will have access to a teamId param in the component, that value will be set when the route is set in the referring component

// $teamId.tsx
import { Link, useOutletContext, useParams } from "remix";

export default function Team() {
  // get list of teams from context
  const [teams] = useOutletContext() as any;

  // the parameter is derived from the name of the file
  const { teamId } = useParams();

  // use parameter and the context to get specific team
  const team = teams[parseInt(teamId as string) - 1];

  return (
    <div style={{ padding: 16 }}>
      <p>{team?.name}</p>
      {team?.players?.map((p: any) => (
        <div style={{ paddingTop: 10 }}>
          <Link to={`/teams/${teamId}/player/${p.id}`}>
            <div>{p.name}</div>
          </Link>
        </div>
      ))}
      <div style={{ paddingTop: 16 }}>
        <Link to="/teams">
          <button type="button" className="button">
            Back
          </button>
        </Link>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This pattern in the code above should start to look familiar at this point since it is very similar to how we displayed the list of teams in a nested component.

Finally our last route /teams/$teamId/player/$playerId will show us the specific player.

import { Link, useOutletContext, useParams } from "remix";

export default function Player() {
  const [teams] = useOutletContext() as any;
  const { teamId, playerId } = useParams();

  const team = teams[parseInt(teamId as string) - 1];
  const player = team.players[parseInt(playerId as string) - 1];

  return (
    <div style={{ padding: 16 }}>
      <p>{team?.name}</p>
      <p>{player?.name}</p>

      <div style={{ paddingTop: 16 }}>
        <Link to={`/teams/${teamId}`}>
          <button type="button" className="button">
            Back
          </button>
        </Link>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Links

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .