Im trying to figure out how to structure a Router to use different routes for admin, user and public.
I have seen this post and the answer describing a key cloak - but I haven't been able to make sense of it.
I've seen this code sandbox which looks logical to me, but I'm having trouble incorporating it.
I have a constants file where I and defining routes as:
export const NEWBLOG = '/admin/newblog';
export const VIEWBLOG = '/viewblog';
I'm importing that into my App.js and then wanting to define different consts for Admin, User and Public as follows:
import * as ROUTES from '../../util/constants/Routes';
import NewBlog from '../../components/blog/admin/New';
// admin routes
const Admin = ({ match }) => (
<React.Fragment>
<Route path={`${match.path}/${ROUTES.NEWBLOG}`} component={NewBlog} />
<Route path={`${match.path}/2`} render={() => <h2>test</h2>} />
</React.Fragment>
);
// authenticated user routes
const Other = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/2`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/2`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
// public routes
const Public = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/2`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/2`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
Then inside the router statement I have:
const App = () => (
<Router>
<Navigation />
<Switch>
<Route path="/a" component={Admin} />
<Route path="/u" component={Other} />
<Route path="/p" component={Public} />
<Route
component={({ location }) => {
return (
<div
style={{
padding: "50px",
width: "100%",
textAlign: "center"
}}
>
<ErrorMessage />
</div>
);
}}
/>
</Switch>
</Router>
);
export default App;
This all works until I try to use the routes constants inside the back ticks part of the Admin constant.
I can't seem to use that approach.
Can anyone help with a source of reference materials to find a way through this?
There are few things you need to know
Child Routes will render only when the Parent route path is matched
For the Child Route the path needs to be the path that matched the parent + the child route path
You can write wrappers over route which are responsible for deciding if the user is authenticated or an admin
In all such scenarios you need to store the user authentication state within state, context or redux store.
When you render the Route in Admin like
<Route path={`${match.path}/${ROUTES.NEWBLOG}`} component={NewBlog} />
The path to the component actually becomes /a/admin/newBlog which is actually incorrect
Overall you can change your code to something like this
App.js
const App = () => (
<Router>
<Navigation />
<Switch>
<Route path="/admin" component={Admin} />
<Route path="/user" component={Other} />
<Route path="/public" component={Public} />
</Switch>
</Router>
);
AuthRoute.js
const AuthRoute = (props) => {
const {path, match, component: Component, render, ...rest} = props;
const {user, isLoading} = useContext(AuthContext); // Assuming you use context to store route, you can actually get this values from redux store too.
return (
<Route
{...rest}
path={`${match.path}${path}`}
render={(routerProps) => {
if(isLoading) return <div>Loading...</div>
if(!user) return <div>Not Authenticated</div>
return Component? <Component {...rest} {...routerProps} /> : render(routerProps)
}}
/>
}
An adminRoute needs to both check whether the user is admin as well as check if he is authenticated or not so you component would look like
AdminRoute.js
const AdminRoute = (props) => {
const {path, match, ...rest} = props;
const {user, isLoading} = useContext(AuthContext); // Assuming you use context to store route, you can actually get this values from redux store too.
return (
<Route
{...rest}
path={`${match.path}${path}`}
render={(routerProps) => {
if(isLoading) return <div>Loading...</div>
if(!user) return <div>Not Authenticated</div>
if(user.role !== "admin") return <div>Need to be an admin to access this route</div>
return Component? <Component {...rest} {...routerProps} /> : render(routerProps)
}}
/>
}
Now you can use the above two components to separate out the Admin and Auth Routes
Also keep in mind that AuthRoutes and public routes paths cannot be the same
Route constants
export const NEWBLOG = '/newblog';
export const VIEWBLOG = '/viewblog';
Routes
import * as ROUTES from '../../util/constants/Routes';
import NewBlog from '../../components/blog/admin/New';
// admin routes
const Admin = (props) => (
<React.Fragment>
<AdminRoute {...props} path={ROUTES.NEWBLOG} component={NewBlog} />
<AdminRoute {...props} path='/2' render={() => <h2>test</h2>} />
</React.Fragment>
);
// authenticated user routes
const Other = (props) => (
<React.Fragment>
<Switch>
<AuthRoute {...props} path={'/3'} render={() => <h2>one</h2>} />
<AuthRoute {...props} path={'/4'} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
// public routes
const Public = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/5`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/6`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
Related
Learning typescript and trying to convert one of my other react projects to typescript to learn it.
I am getting the error on the routes in my App.tsx file - Type '{ history: History<unknown>; location: Location<unknown>; match: match<any>; staticContext?: StaticContext | undefined; }' has no properties in common with type 'IntrinsicAttributes'.
the name prop is also throwing a typescript error, but for now I commented it out to tackle one problem at a time. Here is my App.tsx.
import React from "react";
import Toaster from "./reusable/Toaster.js";
import { HashRouter, Route, Switch } from "react-router-dom";
import { SocketContext, socket } from "./SocketContext";
import "./scss/style.scss";
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
);
// Containers
const TheLayout = React.lazy(() => import("./containers/TheLayout"));
const Login = React.lazy(() => import("./views/auth/Login.js"));
const Logout = React.lazy(() => import("./views/auth/Logout.js"));
const Register = React.lazy(() => import("./views/auth/Register.js"));
const Activate = React.lazy(() => import("./views/auth/Activate.js"));
const ResetPass = React.lazy(() => import("./views/auth/ResetPass.js"));
const Page404 = React.lazy(() => import("./views/page404/Page404"));
const App = () => {
return (
<HashRouter>
<Toaster />
<React.Suspense fallback={loading}>
<SocketContext.Provider value={socket}>
<Switch>
<Route
path="/reset-password"
// name="Reset Password"
render={(props) => <ResetPass {...props} />}
/>
<Route
path="/logout"
// name="Logout"
render={(props) => <Logout {...props} />}
/>
<Route
path="/activate"
// name="Activate"
render={(props) => <Activate {...props} />}
/>
<Route
path="/register"
// name="Register"
render={(props) => <Register {...props} />}
/>
<Route
path="/login"
// name="Login"
render={(props) => <Login {...props} />}
/>
<Route
path="/404"
// name="Not Found!"
render={(props) => <Page404 {...props} />}
/>
<Route
path="/"
// name="Home"
render={(props) => <TheLayout {...props} />}
/>
</Switch>
</SocketContext.Provider>
</React.Suspense>
</HashRouter>
);
};
export default App;
This is a wild guess but is react-router passing in props about the location and history to your components (via the render-props call) but they don't declare that it accepts those properties, so Typescript is telling you that the attempt to push react-router shaped props into components which don't have them will fail.
You need import RouteComponentProps from react-router-dom
import { RouteComponentProps } from 'react-router-dom';
Define custom props for your route
interface RouteProps {
name: string
}
And use it like this on your App component
const App: React.FC<RouteComponentProps<RouteProps>> = () => {
return (
...
<Route
path="/reset-password"
name="Reset Password"
render={(props) => <ResetPass {...props} />}
/>
...
);
};
export default App;
Make sure you have #types/react-router-dom installed and this should fix both the name issue too.
I have a login page which is a public route and a dashboard page which is a private route as follows:
App.js:
function App() {
return (
<Router>
<div className="App">
<Switch>
<PublicRoute path="/" exact component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</div>
</Router>
);
}
And this is my private route component:
let store = require("store");
const PrivateRoute = ({component: Component, ...options}) => {
// const finalComponent = user != null && user.auth == true ? component : Login;
const [userData, setUserData] = useState({});
useEffect(() => {
setUserData(store.get("userData"));
}, []);
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Component {...props} />
:
<Redirect to="/"/>
}
/>
);
};
export default PrivateRoute;
Now the problem that I am facing is that if I open the dashboard localhost:9999/dashboard none of my components are visible because they are routed inside the dashboard component.
So when I open this localhost:9999/dashboard I want react to redirect to localhost:9999/dashboard/home and also show the components if the user is logged in.
I tried to do this :
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Redirect to="/dashboard/home" component={<Component {...props}} />
:
<Redirect to="/"/>
}
/>
);
This did route to the home page but nothing was visible at all. I mean the components inside dashboard weren't visible at all as well.
This is what I am getting. You see it redirects but doesn't show anything:
I believe that you could be more specific about which file is the code that you are showing.
But I feel that what you are trying to accomplish something like this:
PrivateRouter
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({component: Component, restricted, ...rest}) => {
return (
<Route {...rest} render={props => (
user.auth == true ?
<Redirect to="/dashboard" />
: <Redirect to="/dashboard/home" />
)} />
);
};
export default PrivateRoute;
In my app, I'd like to match all routs that end with #something.
/map#login
/info#login
and
/map#register
/map/one#register
/info#register
/info/two#register
So I can show component as popup on top of the content. How this can be done?
I found a solution for this case. It was inspired from this question in stackOverflow. Using HashRoute wrapper for Route and showing component based on location.hash.
const HashRoute = ({ component: Component, hash, ...routeProps }) => (
<Route
{...routeProps}
component={({ location, ...props }) =>
location.hash === hash && <Component {...props} />
}
/>
);
export default class App extends Component {
constructor() {
super();
}
render() {
return (
<div className='App'>
<Router history={history}>
<HashRoute hash='#login'component={Login} />
<HashRoute hash='#register' component={Register} />
<Switch>
<Route exact path='/map' component={Map} />
<Route exact path='/info' component={Info} />
</Switch>
</Router>
</div>
);
}
}
Updating/improving from the other answer here. It would better to not use the component prop as it won't create new instance of the routed component each time the Route is rendered for any reason. The custom HashRoute component should return valid JSX, either a Route component or null.
Example:
const HashRoute = ({ hash, ...routeProps }) => {
const location = useLocation();
return location.hash === hash
? <Route {...routeProps} />
: null
};
...
<Router>
<HashRoute hash='#login' component={Login} />
<HashRoute
hash='#register'
render={props => <Register {...props} otherProp />}
/>
<HashRoute hash='#something'>
<Register otherProp />
</HashRoute>
<Switch>
<Route path='/map' component={Map} />
<Route path='/info' component={Info} />
</Switch>
</Router>
I have this Router, any page that should has the Layout is wrapped with the withLayout HOC.
I need to pass to some of the pages the user context, how can I add a user prop?
const withLayout = () => Component => props => (
<div css={pageWrap}>
<Header user={props.user} />
<Component {...props} />
</div>
);
export default function Router() {
return (
<AuthConsumer>
{({ user }) => (
<Switch>
<Route exact path="/" component={withLayout()(Home, { user })} />
<Route exact path="/page1" component={withLayout()(Page1)} />
<Route exact path="/page2" component={withLayout()(Page2)} />
</Switch>
)}
</AuthConsumer>
);
}
I think you have problem with your withLayout. It should be:
const withLayout = () => (Component, props = {}) => (
<div css={pageWrap}>
<Header user={props.user} />
<Component {...props} />
</div>
);
What's AuthConsumer?
You could use contextType = AuthContext inside your page components. [from]
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* render something based on the value */
}
}
I was able to get this to work like this:
<Route exact path="/" render={props => withLayout()(Home)({ ...props, user })} />
I'm looking for a way to do some route protection with react-router-4. Looking at an example in the documentation, they create a Component which is rendered like this:
<PrivateRoute path="/protected" component={Protected} />
and the privateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
I don't really understand why I should want to pass the "Protected" component as a property and then have to take care of spreading all the ...props and ...rest.
Before I was reading this doc (and other example), I created the following code, which just nest the routes in another component which takes care of the authentication part.
Because my example (which seems to work perfectly well), looks way more simplistic, I must be missing something.
Are there any downsides on this approach?
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import Nav from './Nav';
// dummy
const Auth = {
isAuthenticated: () => { return true; }
}
const Home = () => <h1>Home</h1>
const SignIn = () => <h1>SignIn</h1>
const About = () => <h1>About</h1>
class PrivateOne extends Component {
render() {
console.log(this.props);
return <h1>Private</h1>
}
}
const PrivateTwo = () => <h1>PrivateTwo</h1>
const PrivateThree = () => <h1>PrivateThree</h1>
const NotFound = () => <h1>404</h1>
const Private = ({isAuthenticated, children}) => {
return(
isAuthenticated ? (
<div>
<h1>Private</h1>
{children}
</div>
) : (
<Redirect to={{
pathname: '/sign_in',
}}/>
)
)
}
const App = () =>
<div>
<Router>
<div>
<Nav />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/sign_in" component={SignIn} />
<Private isAuthenticated={Auth.isAuthenticated()}> {/* or some state later on */}
<Route path="/private1" component={PrivateOne} />
<Route path="/private2" component={PrivateTwo} />
<Route path="/private3" component={PrivateThree} />
</Private>
<Route component={NotFound} />
</Switch>
</div>
</Router>
</div>
export default App;