Closed ratoi-crysty closed 1 year ago
This is still in progress, depending on the Middleware API: https://github.com/remix-run/react-router/discussions/10327#discussioncomment-5581679
It's strange to recommend a new way of doing things that doesn't yet seem feasible.
There is a great example here by Bob Ziroll from freecodecamp. https://www.youtube.com/watch?v=nDGA3km5He4&t=1747s
@onattech Are you sure the link/timestamp is correct? I could not find anything regarding createBrowserRouter
I was looking into this and wanted to leave an example for simple folk like myself. I was able to implement a <ProtectedRoute/>
workflow onto the 'createBrowserRouter' react-router-remix.
const root = ReactDOM.createRoot(container!); // createRoot(container!) if you use TypeScript
const dummyUser = {loggedIn: true, token: ''}
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout><ProtectedRoute user={dummyUser}></ProtectedRoute></AppLayout>,
children: [
{
path: '/',
element: <p>hello '/home'</p>
},
{
path: '/page1',
element: <p>hello '/page1'</p>
},
{
path: '/page2',
element: <p>hello '/page2'</p>
},]
},
{
path: '/login',
element: <AppLayout><LoginPage/></AppLayout>
}
])
root.render(
<React.StrictMode>
<RouterProvider router={router} ></RouterProvider>
</React.StrictMode>
);
Component:
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
const ProtectedRoutes = () => {
// TODO: Use authentication token
const localStorageToken = localStorage.getItem("token");
return localStorageToken ? <Outlet /> : <Navigate to="/login" replace />;
};
export default ProtectedRoutes;
Routes:
export const router = createBrowserRouter(
[
{
path: "/",
element: <Home />,
},
{
path: "/login",
element: <Login />,
},
{
element: <ProtectedRoutes />,
children: [
{
path: "/route1",
element: <Screen1 />,
},
{
path: "/route2",
element: <Screen2 />,
},
{
path: "/route3",
element: <Screen3 />,
},
],
},
], { basename: "/app" },
);
Provider:
<RouterProvider router={router} />
https://stackoverflow.com/a/66289280/8690857
It doesn't matter if you are using a routes configuration array with the useRoutes
hook, or if you pass the same object to one of the Data router creation functions, the routing code is the same.
In other words, given:
const PrivateRoutes = () => {
const location = useLocation();
const { authLogin } = /* some auth state provider */;
if (authLogin === undefined) {
return null; // or loading indicator/spinner/etc
}
return authLogin
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
}
const routes = [
{
path: "/",
element: <PrivateRoutes />,
children: [
... protected routes ...
],
},
{
path: "/login",
element: <Login />,
},
{
path: "*",
element: <PageNotFound />
},
];
You can do either of the following:
const appRoutes = useRoutes(routes);
return <BrowserRouter>{appRoutes}</BrowserRouter>;
or
const router = createBrowserRouter(routes);
return <RouterProvider router={router} />;
@awreese My auth state provider is nested under RouterProvider so there is no context for it.
@awreese My auth state provider is nested under RouterProvider so there is no context for it.
@matija2209 It's fine for context providers to be rendered within the RouterProvider
component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.
The example below shows how to create protected routes using createBroweserRouter and using Loader to pass the authenticated data. https://github.com/remix-run/react-router/tree/main/examples/auth-router-provider
The example below shows how to create protected routes using createBroweserRouter and using Loader to pass the authenticated data. https://github.com/remix-run/react-router/tree/main/examples/auth-router-provider
I do not understand the need for the root loader to check for user authentication if I can get the same infos from the AuthProvider object?
@awreese My auth state provider is nested under RouterProvider so there is no context for it.
@matija2209 It's fine for context providers to be rendered within the
RouterProvider
component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.
But in your example, your parent component PrivateRoutes
is also your consumer component. Shouldn't your parent component have the auth provider wrapped around PrivateRoutes?
I have refined my script a little more since I originally posted on this thread, using the following components:
AuthContext
, which fires off a call to a 3rd-party authentication service, in a new-window.<ProtectedRoute/>
wrapper, which expects auth information as an application specific cookie.<ErrorPage/>
, which reads in state from useNavigate
and displays any errors / msgs from a previous action / page.index.tsx
, to provide an example of how I've composed them together.
// - - - - - AuthContext.tsx - - - - -
import React, { createContext, useContext, useState } from 'react';
import { UrlObject } from 'url';
interface AuthContextProps {
isAuthenticated: boolean;
updateAuthentication: (value: boolean) => void;
authEndpoint : UrlObject;
}
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
export const login = (obj : UrlObject ) => {
if(!obj.hostname) throw new Error('Login failed, please provide an authentication url to the application config file.')
else window.open(obj.hostname , '_blank')
}
export const AuthProvider: React.FC<{ children: React.ReactNode, authUrl:string, isDev:boolean }> = ({ children, authUrl, isDev }) => {
//Ignore authentication requirement in development mode
const [isAuthenticated, setIsAuthenticated] = useState
return
export const useAuthContext = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuthContext must be used within an AuthProvider'); } return context; };
// - - - - - ProtectedRoute.tsx - - - - - import React, { useEffect, useState } from "react"; import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useAuthContext } from "./AuthContext"; //import AppLayout //import getCookie
//wraps navigation actions with a check for the application auth cookie. type ProctectRouteProps ={ children?: React.ReactNode isDev: boolean; cookieName: string, landingPath: string, errorPath: string } const ProtectedRoute = ({ children , isDev, cookieName, errorPath, landingPath } : ProctectRouteProps) => { const location = useLocation(); const navigate = useNavigate() const [hasLoaded, setHasLoaded] = useState(false) const { isAuthenticated, updateAuthentication } = useAuthContext();
if(!cookieName) navigate(errorPath, {state:{
error: 'Authentication cookie identifier was not provided. Please review .env
variables.'
}})
useEffect(() => {
// Check app cookie
const cookieValue = getCookie()
const exists = !!cookieValue;
if(isDev){
console.log((Application started in development mode. Bypassing 'ProtectedRoute'.
))
updateAuthentication(true)
}
else updateAuthentication(exists)
setHasLoaded(true)
}, [location.pathname, cookieName, updateAuthentication, isDev]); // Run the check on every route change
if (!isAuthenticated) {
//Direct user to home page on first login.
if(!hasLoaded) return <Navigate to={landingPath} replace state={{error: 'No authentication cookie was found, please complete the login process.'}}/>;
else return <Navigate to={errorPath} replace state={{error: 'No authentication cookie was found, please complete the login process.'}}/>;
}
return (
)
};
export default ProtectedRoute;
// - - - - - ErrorPage.tsx - - - - - import React from "react"; import { useLocation, useNavigate } from "react-router-dom"; // import Button
const isDev = process.env.REACT_APP_NODE_ENV === 'development'
const hostname = ${isDev ? process.env.REACT_APP_DEV_API_URL : process.env.REACT_APP_PROD_API_URL}
type ErrorPageProps = { error?: string; } const ErrorPage = (props:ErrorPageProps) => { const navigate = useNavigate(); const location = useLocation();
const navMsg = location.state?.error
const msg = navMsg ? <p className="text-lg">{navMsg}</p> :
props.error ? <p className="text-lg">{props.error}</p>:
<p className="text-lg">The targeted page <b>"{location.pathname}"</b> was not found, please confirm the spelling and try again.</p>
return (
<section id="Error" className="w-full h-full flex flex-col items-center">
{msg}
<span className="flex flex-row justify-center items-center space-x-8 my-10 ">
<Button id='backButton' onClick={()=>navigate(-1)}>
Return to Previous Page
</Button>
<Button id='homeButton' onClick={()=>navigate('/home')}>
Return to Home Page
</Button>
<Button id='logout' onClick={()=>{
const endpoint = `${hostname}/logout`
window.open(endpoint, '_blank')
}}>
Reset Authentication
</Button>
</span>
</section>
)
}
export default ErrorPage;
// - - - - - index.tsx - - - - -
import React from "react";
import ReactDOM from 'react-dom/client';
//import your application pages
//import
const isDev = process.env.REACT_APP_NODE_ENV === 'development' const authUrl = isDev ? process.env.REACT_APP_DEV_AUTH_URL : process.env.REACT_APP_PROD_AUTH_URL const container = document.getElementById('root'); const root = ReactDOM.createRoot(container!); // createRoot(container!) if you use TypeScript
const router = createBrowserRouter([
{
path: '/',
element: <ProtectedRoute isDev={isDev} cookieName={process.env.REACT_APP_COOKIE_NAME || ''} landingPath="/home" errorPath="/error"/>,
children: [
{
path: '/somePage',
element: <>page</>
},
]
},
{
path: '/home',
element:
])
root.render(
@awreese My auth state provider is nested under RouterProvider so there is no context for it.
@matija2209 It's fine for context providers to be rendered within the
RouterProvider
component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.But in your example, your parent component
PrivateRoutes
is also your consumer component. Shouldn't your parent component have the auth provider wrapped around PrivateRoutes?
@alxvallejo This is a very basic and trivial code example. The assumption here is that the context provider component just exists somewhere higher in the ReactTree... whether it is directly wrapping the PrivateRoutes
component, the RouterProvider
component, or some other ancestor component is a bit irrelevant, it just needs to exist as an ancestor component to any component consuming it's value, e.g. the normal React Context usage.
Could you kindly confirm whether this private route is functioning correctly?
Additionally, could you provide guidance on how to utilize this private route, as it is currently displaying an error?
A Route is only ever to be used as the child of
const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) =>
isAuthenticated() ? (
<Component {...props} />
) : (
<Navigate to="/Login" />
)
}
/>
);
};
export const pageRoutes = [
{ path: "/", element: <PrivateRoute component={Home} />, title: "Home" },
{ path: "/PurchaseCreate", element: <PurchaseCreate />, title: "Purchase Create" },
{ path: "/PurchaseList", element: <PurchaseList />, title: "Purchase List" },
// other mappings ...
]
export const loginRoutes = [
{ path: "/Login", element: <Login /> },
{ path: "/Register", element: <Register /> },
{ path: "/ResetPassword", element: <ForgotPassword /> },
]
export const emptyRoutes = [
{ path: "*", element: <Page404 /> },
]
export const pageRouter = createBrowserRouter([
{
element: <DashboardLayout />,
children: pageRoutes,
},
{
element: <LoginLayout />,
children: loginRoutes,
},
{
element: <EmptyLayout />,
children: emptyRoutes,
},
])
Describe what's incorrect/missing in the documentation
Having that currently, this is the recommended way of using react-router, we need an example of how to protect the routes created with createBrowserRouter.
Currently, the only example for auth available, is the old one using JSX
<BrowserRouter/>
and<Route/>
.