React Router based on dynamic context values - reactjs

I hope all you are ding good.
I am developing a react(typescript) application in which I have to handle authentication and authorization.
I am following this pattern.
IAuthContext (will be loaded during startup or when user change their state)
export interface IAuthContext {
isAuthenticated: boolean;
isInitialized: boolean;
user: firebase.User | null;
permissions: object;
landingPage: string;
isOnBoardingCompleted: boolean;
}
Routes.js
const routerConfig = [
{
key: "key_",
path: '/login',
component: Login,
isPrivate : false
},
{
key: "dashboard",
path: '/dashboard',
component: Frame,
content: AnalyticsDashBoard,
isPrivate : true
},
App.tsx
return (
<BrowserRouter>
<div>
<Switch>
{routes.map((route) =>{
return (route.isPrivate ?
<PrivateRoute {...route} exact/>
:
<Route path={route.path} component={route.component} key={route.key} exact/>)
})}
</Switch>
</div>
</BrowserRouter>
);
PrivateRoute.tsx
return (
<Route
{...rest}
render={(routeProps) =>
props.context.isAuthenticated ? (
<Component {...routeProps} content={props.content}/>
) : (
<Redirect
to={{
pathname: '/login',
state: { from: routeProps.location }
}}
/>
)
}
/>
);
Inside privateRoute I have access to role and permissions along with landing page(if user didnt complete registration after login, he has to be redirected to registration page). Also I need to give all possible combinations in privateroute based on authentication since it will load only once.
I tried to do this in private route
if (props.context.isAuthenticated && !props.context.isOnBoardingCompleted) {
console.log("Going to redirect here to onboard--", props.context.landingPage);
return <Route path={props.context.landingPage} component={OnBoarding}/>
}
But this is not working since only the URL is changing. Also if the user is done with onboarding, I might not have a Route for it since all router controls already created with first-time values.
Please advise me on how can I handle this? All I need to do it is to have some sort of interceptor where I route/redirect to pages based on dynamic context values.
Expectation:
Just like scenario above, there are multiple roles and permissions and all those conditions should be checked here.

<Route path={props.context.landingPage} component={OnBoarding}/> should be defined in your Router component.
You can use a nested ternary, though this can lead to readability issues.
return (
<Route
{...rest}
render={routeProps =>
props.context.isAuthenticated ? (
props.context.isOnBoardingCompleted ? (
<Component {...routeProps} content={props.content} />
) : (
<Redirect
to={{
pathname: "/landing", // or "/onboard" whatever the route is
state: { from: routeProps.location }
}}
/>
)
) : (
<Redirect
to={{
pathname: "/login",
state: { from: routeProps.location }
}}
/>
)
}
/>
);

May be you can organize your code like below. It will help you separate different routes and handles their role/auth checks in a clean readable manner.
function App() {
return (
<BrowserRouter>
<div>
<Switch>
<Route component={PublicContainer}/>
<Route component={PrivateContainer}/>
</Switch>
</div>
</BrowserRouter>
);
}
const PrivateContainer = () => (
<div>
<NavBar/>
<Switch>
//routes with auth checks
</Switch>
</div>
);
const PrivateContainer = () => (
<div>
<NavBar/>
<Switch>
//Free routes
</Switch>
</div>
);

Related

React-Router-Dom 6 - How to dynamically render a component?

My old method:
<Route
key={i}
path={path}
render={(props) => {
if (!localStorage.getItem("token")) {
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
}
return (
<AuthLayout>
<Component {...props} />
</AuthLayout>
);
}}
/>
Replacing render with the new element gives me:
Functions are not valid as a React child. This may happen if you return a Component instead of from render
Apparently the new API simply expects:
<Route
key={i}
path={path}
element={
<Component />
}
/>
What I'm really trying to accomplish is to dynamically render the component as such:
{authProtectedRoutes.map(({ path, Component }, i) => {
<Route
key={i}
path={path}
element={
// If no auth token, redirect to login
if (!token) {
<Navigate to="/login" />
} else {
<AuthLayout>
<Component />
</AuthLayout>
}
}
/>
})}
Not sure how to do this ...
EDIT:
My array of components is as such:
const authProtectedRoutes = [
{ path: "/dashboard", Component: Dashboard },
{ path: "/pages-starter", Component: StarterPage },
When I try to return Component in my loop I get:
React.jsx: type is invalid -- expected a string (for built-in
components) or a class/function (for composite components) but got:
undefined. You likely forgot to export your component from the file
it's defined in, or you might have mixed up default and named imports.
element={
// If no auth token, redirect to login
if (!token) {
<Navigate to="/login" />
} else {
<AuthLayout>
<Component />
</AuthLayout>
}
}
You can't do an if in the middle of jsx, but you can do a conditional operator:
element={!token ? (
<Navigate to="/login" />
) : (
<AuthLayout>
<Component />
</AuthLayout>
)}
The element prop expects a ReactNode (a.k.a. JSX) and not javascript (i.e. the if-statement).
Since it seems you render your authenticated routes in bulk a more optimal solution would be to wrap them all in a single AuthLayout component that checks the token. Instead of rendering the children prop it renders an Outlet for nested routes to be rendered into.
Example:
const AuthLayout = ({ token }) => {
// ... existing AuthLayout logic
return token
? (
<div /* awesome auth layout CSS style */>
...
<Outlet /> // <-- nested routes render here
</div>
)
: <Navigate to="/login" />;
};
Don't forget to return the Route from the map callback.
<Route element={<AuthLayout token={token} />}>
{authProtectedRoutes.map(({ path, Component }) => (
<Route key={path} path={path} element={<Component />} />
))}
</Route>
Nice routing-related question. First of all, I found useful code example from react-router-dom's github: https://github.com/remix-run/react-router/blob/2cd8266765925f8e4651d7caf42ebe60ec8e163a/examples/auth/src/App.tsx#L104
Here, instead of putting some logics inside "element" or "render" authors suggest to implement additional RequireAuth component and use it in routing setup like following:
<Route
path="/protected"
element={
<RequireAuth>
<SomePageComponent />
</RequireAuth>
}
....
This approach would allow to incapsulate auth-related checks inside this new RequireAuth component and as a result make your application routing setup more "lightweight"
As a "brief" example, I created following piece of code you could reference to:
function App() {
return (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
}
const RequireAuth = ({ children }) => {
const token = localStorage.getItem('token');
const currentUrl = useHref();
return !token ? (
<Navigate to={`/login?redirect=${currentUrl}`} />
) : (
children
);
};
const authProtectedRoutes = [
{ path: '/', component: PaymentsPage },
{ path: '/user', component: UserInfoPage },
];
const AppRoutes = () => (
<Routes>
{authProtectedRoutes.map((r) => (
<Route
key={r.path}
path={r.path}
element={
<RequireAuth>
<AuthLayout>
<r.component />
</AuthLayout>
</RequireAuth>
}
/>
))}
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);

React App redirects to route "/" when I try to navigate by changing url manually

I am creating a React app, that stores the users role (there are two possible roles 0, and 1 which are being used for conditional rendering) in a "global" React Context. The role gets assigned upon login. I am also using React Router to handle Routing, and wrote a ProtectedRoute component. My Problem is the following: When I am navigating via the NavBar all works perfectly fine, but when I enter e.g. /home into the url I get redirected to the LoginPage(which is the standard route when the role is not set to 0 or 1) and I can not access the other Routes anymore. However, the user does not get logged out (The session in the database is not being deleted), the app just seems to forget the global state "role", which is used to determine whether the user is allowed to access the individual routes. I am afraid my knowledge of the DOM and Router is too limited to solve this problem.
function App() {
return (
<AuthProvider>
<Router>
<NavigationBar />
<AuthContext.Consumer>
{context => (
<React.Fragment>
{!context.isAuthenticated() ? <Jumbotron/> : null}
</React.Fragment>
)}
</AuthContext.Consumer>
<Layout>
<Switch>
<Route exact path="/" component={() => <LandingPage />} />
<ProtectedRoute path="/home" component={Home} />
<ProtectedRoute path="/about" component={About}/>
<ProtectedRoute path="/account" component={Account} />
<ProtectedRoute path="/calender" component={Calender} />
<ProtectedRoute path="/xyz" component={Xyz} />
<ProtectedRoute path="/wasd" component={wasd} role={0} />
<Route component={NoMatch} />
</Switch>
</Layout>
</Router>
</AuthProvider>
);
}
export default App;
export const ProtectedRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated, getRole } = useContext(AuthContext);
if (rest.role === 0) {
return (
<Route
{...rest}
render={props =>
getRole() === 0 ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/404",
state: {
from: props.location
}
}}
/>
)
}
/>
);
} else if (rest.role === 1) {
return (
<Route
{...rest}
render={props =>
getRole() === 1 ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/404",
state: {
from: props.location
}
}}
/>
)
}
/>
);
} else {
return (
<Route
{...rest}
render={props =>
isAuthenticated() ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
)
}
/>
);
}
};
import React from "react";
const AuthContext = React.createContext();
export default AuthContext;
class AuthProvider extends Component {
constructor() {
super();
this.state = {
role: 2, //none=2
name: "",
email: ""
};
}
render() {
return (
<AuthContext.Provider
value={{
state: this.state,
isAuthenticated: () => {
if (this.state.role === 1 || this.state.role === 0) {
return true;
}
return false;
},
setRole: newRole =>
this.setState({
role: newRole
}),
getRole: () => {
return this.state.role;
},
setName: newName =>
this.setState({
name: newName
}),
getName: () => {
return this.state.name;
},
setEmail: newEmail =>
this.setState({
email: newEmail
}),
getEmail: () => {
return this.state.email;
},
}}
>
{this.props.children}
</AuthContext.Provider>
);
}
}
export default AuthProvider;
If you are entering the url directing into the browser, React will reload completely and you will lose all state whether 'global' or otherwise. The most likely scenario is that your router is trying to validate your ability to view a component before you have your auth data.
You don't include how you get your auth session from the database, but even if you refetch the auth session, there is going to be a period where your app has mounted, but you don't have the response yet. This will cause your protected route to believe you are unauthorized, and redirect to the fallback path.
Try adding a check either inside your protected route or before the router itself, that blocks rendering until your auth data is loaded. Although upon reading your question again, it seems like you may not be refetching your logged in user at all.

React Router Redirect not working in Private Route

I have this private route component that is used to render a component only is the user is logged in, else it should redirect to the login page.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
authToken()
? <Component {...props} />
: <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)} />
)
export default withRouter(PrivateRoute);
and this is my main app:
<BrowserRouter>
<div className="wrapper">
<Switch>
<Route path="/login" component={LoginPage} />
<>
<div className="dashboard">
<SideBar />
{permittedEvents &&
<div className="content-area">
<PrivateRoute exact path="/" component={Dashboard} />
<PrivateRoute exact path="/calendar" component={Calendar} />
</div>
}
</div>
</>
</Switch>
</div>
</BrowserRouter>
for some reason the redirect is being ignored completely and when the user is not logged in, the Sidebar gets rendered but nor the content or the login page get rendered.
I have tried returning only the redirect in te Private route to force te redirect and check whether it was something wit my authentication. But the redirect doesn't seem to be working no matter where its included.
You don't need Route
class PrivateRoute extends React.Component {
constructor(props) {
super(props);
}
render() {
const { component: Component, ...rest } = this.props;
const redirectPath = (<Redirect to={{
pathname: "/login",
state: {
from: this.props.location.pathname
}
}}/>);
if (!ok) {
return redirectPath;
}
return <Component {...this.props} />;
}
};
export default withRouter(PrivateRoute);

How to show 404 Page when user is typing any URL on login page if user is not authenticated?

I want user to be blocked by accessing invalid URL from login screen if user is not authenticated, for instance, consider user is on login screen and if user tries to access any random url localhost:3000/kanskd, he/she should be redirected to login screen. I am able to achieve what i need by placing NoMatch route component, however, it matches the route inside my application as well and it renders No match for those routes as well[Routes that i am mapping after NoMatch route does not work].
index.js
import Routes from './routes'
<Switch>
<Route exact path="/" render={() => {
if(!store.getState().login.isAvailable) {
return <Redirect to="/login"/>
} else {
return <Redirect to="/dashboard" />
}
}}
/>
<Route exact path="/login" component={Login} />
<Route component={NoMatch} />
{Routes.map((prop, key) => {
return <Route path={prop.path} key={key} component={prop.component}
/>;
})}
</Switch>
NoMatch.jsx
import React from 'react'
import { withRouter } from 'react-router-dom';
const NoMatch = ({ location }) => (
<div>
<h3>No match for <code>{location.pathname}</code></h3>
</div>
)
export default withRouter(NoMatch);
EDIT:
routes/index.js
import Dashboard from "Dashboard/Dashboard.jsx";
var Routes = [{ path: "/", name: "Dashboard", component: Dashboard }];
export default Routes;
Once the user logs in, it routes him to Dashboard and in Dashboard, there are other multiple routes.
So you have to solve 2 things here: show the NoMatch component when there is no match for a url and protect some routes from not logged users.
For the first one you should put your <Route component={NoMatch} /> just before the <Switch>closing tag, think of this like a switch in plain javascript, the last case is always the default case, if there is no other match the default will be executed, same as here.
The second problem requires a bit of extra code, you have to create a component that redirects the user if is not logged in, something like this (this is from the documentation react-router docs):
function PrivateRoute({ component: Component, isLoggedIn,...rest }) {
return (
<Route
{...rest}
render={props =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
Then use this component for protected routes:
<Switch>
<Route exact path="/" render={() => {
if(!store.getState().login.isAvailable) {
return <Redirect to="/login"/>
} else {
return <Redirect to="/dashboard" />
}
}}
/>
<Route exact path="/login" component={Login} />
{Routes.map((prop, key) => {
return <PrivateRoute path={prop.path} key={key} component={prop.component} isLoggedIn={isUserLoggedIn}
/>;
})}
<Route component={NoMatch} />
</Switch>
isUserLoggedIn is a made up variable, you should replace it for you logged in checks methods
Edit:
The path should be /dashboard:
import Dashboard from "Dashboard/Dashboard.jsx";
var Routes = [{ path: "/dashboard", name: "Dashboard", component: Dashboard }];
export default Routes;
if you want to maintain / as your path you should return the dashboard component inside your Route component instead of redirecting:
<Route exact path="/" render={() => {
if(!store.getState().login.isAvailable) {
return <Redirect to="/login"/>
} else {
return <Dashboard/>
}
}}
/>

rewrite react router example in typescript

I try to use typescript to rewrite the example of 'Redirects (Auth)', but meet one problem that the function cannot receive the props as I expect.
Here is my code:
router setting and function of private router:
export default () => (
<Router>
<Switch>
<PrivateRoute exact path="/welcome" component={Welcome}/>
<Route component={NoMatch}/>
</Switch>
</Router>)
const PrivateRoute = (component: any, ...rest: Array<any>) => {
console.log(component, rest)
return (
<Route {...rest} render={props => (
isAuthenticated() ? (
<div>
<Header/>
<Sider/>
<div className="content slide-in">
<component {...props}/>
</div>
</div>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)}
I expect that the param of component is the Component of 'welcome', and the param of rest are other params such as 'exact' and 'path', but actually get the params as in above image.
component:
rest:
Anyone can help me to solve this problem?
Many thanks!

Resources