Reputation: 367
I have seen many questions about this but none seem to work for me because they are old. The packages have been updated and the solutions for this have been stripped out. I do not want to use functional components to solve this.
I'm trying to create a private route with ReactJS and Typescript:
"react": "^17.0.2",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.5",
But when I try to extend a Route:
export class PrivateRoute extends Route<PrivateRouteProps> {
public render(): JSX.Element {
return (
// ...
);
}
}
I get an error saying that I can't extend Route
:
Type '(_props: PathRouteProps | LayoutRouteProps | IndexRouteProps) => ReactElement<any, string | JSXElementConstructor> | null' is not a constructor function type.ts(2507)
And when I try to wrap a component around Route
:
export class PrivateRoute extends Component<PrivateRouteProps> {
public render(): JSX.Element {
return (
// ...
<Route />
// ...
);
}
}
It let's me build and even run but I get a runtime error:
Uncaught Error: A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.
Is there a correct way to do this? If not, is there a workaround for this without using functional components?
EDIT #1:
<Provider store={authStore}>
<BrowserRouter>
<Routes>
<Route path='/' element={<MainPage />} />
<Route path='/login' element={<LoginPage />} />
<PrivateRoute path='/dashboard' element={<DashboardPage />} />
<Route path='*' element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
</Provider>
EDIT #2:
For future reference: I have solved this using @SlothOverlord's answer but translated from functional component. Also, I'm using redux to fill the props.
private.outlet.tsx:
export class PrivateOutlet extends Component<AuthenticationProps> {
public render(): JSX.Element {
const user = this.props.user;
if (user) { // Check if logged in
return (
<>
{this.props.children}
<Outlet />
</>
);
}
return <Navigate to="/login" replace />; // Go back to login if not logged in
}
}
public.outlet.tsx:
export class PublicOutlet extends Component<AuthenticationProps> {
public render(): JSX.Element {
const user = this.props.user;
if (!user) { // Check if logged in
return (
<>
{this.props.children}
<Outlet />
</>
);
}
return <Navigate to="/" replace />; // Go to protected route if logged in
}
}
The routes I kept the same as in the answer.
EDIT #3:
Regarding the [duplicate] status: The question linked does not solve the problem for the versions stated at the beginning of the post. As @SlothOverlord stated at the beginning of his answer:
Routes work different in v6.
Not only that but Redirect
does not exist in version 6. Many things are different. The decision to mark it duplicate was based on nothing except keyword matching of a flagging trigger happy mod.
This site is becoming a haven for self-important, misanthropic bullies that find in moderation a way to punish people for their own frustrations.
Upvotes: 2
Views: 3663
Reputation: 2037
Routes work different in v6.
Note: Functional components example only:
You don't need to pass props to your routes anymore. There are two ways. Render route as outlet or a child. Outlet is basically your nested route, while children are direct children in your <Private/Public Outlet> component
Auth status is checked inside the route, not outside of it as in v5.
There are two examples below. Private outlet renders nested routes if logged in, public route renders children if not logged in.
For typescript, only FC type is needed to pass children, nothing else.
const PrivateOutlet: FC = ({ children }) => {
const { user } = useContext(ContextProvider); //Auth context
return user?.id ? ( //Check if logged in
<>
{children} //This is your children
<Outlet /> //This is your nested route
</>
) : (
<Navigate to="/login" replace /> //Go back to login if not logged in
);
};
const PublicOutlet: FC = ({ children }) => {
const { user } = useContext(ContextProvider); //Auth context
return !user?.id ? ( //Check if logged in
<>
{children} //This is your children
<Outlet /> //This is your nested route
</>
) : (
<Navigate to="/" replace /> //Go to protected route if logged in
);
};
export const MainRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PrivateOutlet />}> //Check auth for all nested routes
<Route index element={<MainPage />} /> //Outlet
<Route path="private" element={<MainPage2 />} /> //Outlet
</Route>
<Route
path="login" //Not nested in "/" Check for auth individually
element={
<PublicOutlet>
<LoginPage /> //Children
</PublicOutlet>
}
/>
//alternative for login
<Route path="login" element={ <PublicOutlet/> }>
<Route index element={<Login />} /> //Render as outlet.
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
};
You can also render nested routes from your other components. Just put "<Outlet/>"
inside of it and it will render nested routes at that position. That way you can only place one dashboard component at the root of your route.
Upvotes: 4