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>
</>
}
}
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.
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"
}
},
...
]
}
]);
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>
)
},
...
]
}
]);