Search code examples
reactjsreact-router-dom

How can I extend the route component to incorporate custom props?


I'm looking to augment react-router-dom's Route with custom properties. Each route should include a 'name' attribute, which will serve as a description displayed at the top of the page.

Ideally, I'd like the Route component to explicitly request and accept these custom properties. This would enable me to access them within middleware responsible for user authentication and page layout assembly.

The provided example below isn't functioning as expected, and I'm currently unsure of the cause. The error I encountered is: "Uncaught Error: [T] is not a component. All component children of must be a or <React.Fragment>".

import { RouteProps, Route as R } from "react-router-dom"

type Props = RouteProps & {
    name?: string
}

export const T: React.FC<Props> = ({...props}) => {
    return <R {...props} />
}
import { BrowserRouter, Routes } from "react-router-dom";
import { T } from "@components/Route"

const RoutesApp = () => {
  return (
    <BrowserRouter>
      <Routes>
        <T path="/" element={<Middleware />} >
          <T path="test" name="This is a page" element={<>test</>} />
        </T>
      </Routes>
    </BrowserRouter>
  );
};

export default RoutesApp;
import { Outlet } from "react-router-dom";

export const Middleware = () => {
  if(...){
    return <>
        <main>
          <h1>{name}</h1>
          <Outlet />
        </main>
    </>
  }
}

Solution

  • AFAIK you cannot extend the Route component and still be able to render it within the Routes or Route components as it will no longer be an instance of Route and fail the invariant check.

    Data APIs

    The Route component can already kind of handle "extra" data passed in the route in the form of the route handle prop. This only works in the newer Data APIs (see Picking a Router). Use the route handle to pass the name property and use the useMatches hook in the Middleware component to compute the match and extract the name.

    Example:

    const Middleware = () => {
      const matches = useMatches();
      const name = matches.filter((match) => match.handle?.name).pop()?.handle.name;
    
      return (
        <>
          <main>
            <h1>{name}</h1>
            <Outlet />
          </main>
        </>
      );
    };
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Middleware />,
        children: [
          ...,
          {
            path: "/test",
            element: <>test</>,
            handle: {
              name: "This is a page"
            }
          },
          ...
        ]
      }
    ]);
    

    Non-Data APIs

    If you don't want to update/migrate to the DATA APIs then you can utilize the Middleware component's Outlet context and a wrapper component to set a route name. Use a local React state to hold the name value and expose a setter callback in the Outlet context. The wrapper component takes a name prop and updates the Middleware state with the current name. Each routed component is wrapped in the wrapper to set the name.

    Example:

    const Middleware = () => {
      const [name, setName] = useState();
    
      return (
        <>
          <main>
            <h1>{name}</h1>
            <Outlet context={{ setName }} />
          </main>
        </>
      );
    };
    
    const Wrapper = ({ children, name }) => {
      const { setName } = useOutletContext();
    
      useEffect(() => {
        setName(name);
      }, [name]);
    
      return children;
    };
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Middleware />,
        children: [
          ...,
          {
            path: "test",
            element: (
              <Wrapper name="This is a page">
                test
              </Wrapper>
            )
          },
          ...,
          
          {
            path: "test",
            element: (
              <Wrapper> // <-- "unset" name
                test 2
              </Wrapper>
            )
          },
          ...
        ]
      }
    ]);