Search code examples
reactjsreact-router-domurl-parameters

How do I use a dynamic URL that won't redirect to error page when the URL doesn't match any of the routes I've declared? - react-router-dom


I am creating a project with react-router-dom where I want the URL to hold an id. The id will be used to fetch information about the item with that id from a database and render it on the page. I've already successfully implemented data loading based on the id in the url.

My issue is that react-router-dom seems to interpret the url with the id as an unknown route and redirects it to my error page.

This is how my routes are set up:

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <Error />,
    children: [
      { path: "/", element: <Home /> },
      { path: "progress", element: <Progress /> },
      { path: "logworkout", element: <LogWorkout /> },
      {
        path: "programs",
        element: <Programs />,
        children: [
          { index: true, element: <Navigate to="myprograms" replace /> },
          { path: "discover", element: <DiscoverPrograms /> },
          {
            path: "myprograms",
            element: <MyPrograms />,
            children: [
              {
                path: ":programID",
                element: <ProgramView />,
                loader: programViewLoader,
              },
            ],
          },
        ],
      },
      { path: "product", element: <Product /> },
      { path: "contact", element: <Contact /> },
    ],
  },
]);

I basically want /programs/myprograms/:programID to render instead of the error page. I think I followed the tutorial on react-router-dom's documentation pretty well and I can't figure out what I'm missing.

Edit: Only <Programs/> in /programs renders an Outlet

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

const Programs = () => {
  return (
    <div className="grid overflow-hidden" style={{gridTemplateColumns: "auto 1fr"}}>
      <SideBar />
      <div className="overflow-auto">
        <Outlet />
      </div>
    </div>
  );
};

export default Programs;

Edit: The issue isn't about the dynamic route but about the loader function I'm using for <ProgramView/>. Apparently, I'm not returning a value in the loader.

import Cookies from "js-cookie";
import { refreshToken } from "../../../../util/auth";

export const programViewLoader = ({ params }) => {
  refreshToken()
    .then((refresh) => {
      return fetch("http://localhost:5000/program/load-program", {
        method: "POST",
        headers: {
          "Content-type": "application/json",
          Authorization: `Bearer ${Cookies.get("accessToken")}`,
        },
        body: JSON.stringify({
          programID: params.programID,
        }),
      });
    })
    .then((response) => {
      if (!response.ok) {
        return response.json().then((data) => {
          throw { error: data.error, status: response.status };
        });
      }
      return response.json();
    })
    .then((data) => {
      console.log(data.program);
      return data.program;
    })
    .catch((error) => {
      return { error: "An error occurred while loading the program." };
    });
};

However the console.log() always prints something so I'm not sure how it isn't returning anything


Solution

  • Only <Programs /> in "/programs" renders an Outlet

    This allows only the immediately nested routes to be rendered into the parent route component's Outlet, e.g:

    • Navigate on the "/programs" index route
    • DiscoverPrograms on "/programs/discover"
    • MyPrograms on "/programs/myprograms"

    Any of these components would need to, yet again, render another Outlet of their own for any nested routes they may be rendering.

    const MyPrograms = () => {
      ...
    
      return (
        ...
    
        <Outlet />
    
        ...
      );
    };
    

    Now ProgramView should be renderable via MyPrograms' Outlet via Programs' Outlet on "/programs/myprograms/:programID".

    The programViewLoader isn't returning a value. The refreshToken function returns a Promise chain, but then you need to actually return the result of the Promise chain from the loader function.

    export const programViewLoader = ({ params }) => {
      return refreshToken() // <-- return Promise chain!
        .then((refresh) => {
          return fetch("http://localhost:5000/program/load-program", {
            method: "POST",
            headers: {
              "Content-type": "application/json",
              Authorization: `Bearer ${Cookies.get("accessToken")}`,
            },
            body: JSON.stringify({
              programID: params.programID,
            }),
          });
        })
        .then((response) => {
          if (!response.ok) {
            return response.json().then((data) => {
              throw { error: data.error, status: response.status };
            });
          }
          return response.json();
        })
        .then((data) => {
          console.log(data.program);
          return data.program;
        })
        .catch((error) => {
          return { error: "An error occurred while loading the program." };
        });
    };