I am using react-router v4 along side react-loadable to async load matched routes.
<LayoutWrapper>
<Route
exact
path="/"
render={props =>
currentUser ? (
<AsycnExamplePage {...props} currentUser={currentUser} />
) : (
<AsycnExamplePage />
)}
/>
<Route
exact
path="/saved"
render={props => <AsycnExamplePage {...props} currentUser={currentUser} />}
/>
<Route
exact
path="/settings"
render={props => (
<AsycnExamplePage {...props} currentUser={currentUser} />
)}
/>
</LayoutWrapper>
AsycnExamplePage is a component wrapped with Loadable from react-loadable, e.g:
Loadable({
loader: () => import("./index"),
loading: props => {
if (props.isLoading) {
NProgress.start();
}
return null;
},
});
Currently, as I route to a different page, react-router will instantly match the route before the async route is resolve. Thereby showing the LoadingComponent from react-loadable.
How can I make it so that react-router will only render the async component after the async component is resolved?
PS: I'm trying to create a page load effect similar to Youtube.
Related
Split my routes into two (for now, going to be four later) separate components.
const RouterComponentOne = () => {
return (
<Switch>
<Route path="/routeA" component={ComponentA} />
<Route path="/routeB" component={ComponentB} />
</Switch>
)
}
const RouterComponentTwo = () => {
return (
<Switch>
<Route path="/routeC" component={ComponentA} />
<Route component={Component404} />
</Switch>
)
}
const RouterDefault = () => {
return (
<>
<RouterComponentOne />
<RouterComponentTwo />
</>
)
}
export default RouterDefault;
When visiting any route from RouterComponentOne the 404 component is rendered (which it shouldn't). It renders on non-existent routes (example /test/123/abc which is correct, and doesn't render on any RouterComponentTwo route.
I added some dummy components to the RouterDefault and the 404 component rendered on all additional paths as well.
In short, the 404 route isn't matched/rendered within the routes of the component where the 404Route is. If put into the RouterComponentOne it won't render on routeA and routeB but will render for routeC
How can I solve this?
You need a property called exact
const RouterComponentOne = () => {
return (
<Switch>
<Route path="/routeA" exact component={ComponentA} />
<Route path="/routeB" exact component={ComponentB} />
</Switch>
)
}
I want to implement the PrivateRoute HOC which will check if the user is logged in then render the corresponding component, otherwise redirect to the login page.
Here is the PrivateRoute HOC:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component, ...rest }) => (
<Route {...rest} render={(props) => (
sessionStorage.getItem('userToken') ? (
React.createElement(component, props)
) : (
<Redirect to={{
pathname: '/login',
}} />
)
)} />
);
export default PrivateRoute;
Here's how I'm using it:
import PrivateRoute from './components/HOC/PrivateRoute';
...
render() {
if (this.props.hasError) return <h1>Sorry, something went wrong...</h1>;
if (this.props.isLoading) return <h1>Loading...</h1>;
return (
<div className="App">
<Switch>
<PrivateRoute exact path="/" component={<Home /> } />
<Route exact path="/login" render={props => <Login {...props} />} />
<Route exact path="/registration" render={props => <Registration {...props} />} />
<PrivateRoute exact path="/account" component={<Account />} />
</Switch>
</div>
);
}
But it throws an error:
I find a GitHub thread where I find a possible answer for why it happens like that, but I don't know how to implement that answer for my case:
I tried some experimental ways to achieve that but everything fails.
React.createElement accepts a type or a React component before it invoked.
React.createElement(
type,
[props],
[...children]
)
The type argument can be either a tag name string (such as 'div' or 'span'), a React component type (a class or a function), or a React fragment type.
const Component = () => <>Hello</>;
// Good
React.createElement(Component)
React.createElement("div")
// Bad
React.createElement(<div/>)
In your case you use the "bad" version.
You should use cloneElement or fix it with a wrapper:
// Good
React.cloneElement(<Component />)
// Or make a wrapper
React.createElement(() => <Component />));
So finally:
// Change to clone
React.cloneElement(component, props)
// **OR** make a wrapper
<PrivateRoute exact path="/" component={() => <Home />} />
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>
);
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>
Please read this properly before marking as duplicate, I assure you I've read and tried everything everyone suggests about this issue on stackoverflow and github.
I have a route within my app rendered as below;
<div>
<Header compact={this.state.compact} impersonateUser={this.impersonateUser} users={users} organisations={this.props.organisations} user={user} logOut={this.logout} />
<div className="container">
{user && <Route path="/" component={() => <Routes userRole={user.Role} />} />}
</div>
{this.props.alerts.map((alert) =>
<AlertContainer key={alert.Id} error={alert.Error} messageTitle={alert.Error ? alert.Message : "Alert"} messageBody={alert.Error ? undefined : alert.Message} />)
}
</div>
The route rendering Routes renders a component that switches on the user role and lazy loads the correct routes component based on that role, that routes component renders a switch for the main pages. Simplified this looks like the below.
import * as React from 'react';
import LoadingPage from '../../components/sharedPages/loadingPage/LoadingPage';
import * as Loadable from 'react-loadable';
export interface RoutesProps {
userRole: string;
}
const Routes = ({ userRole }) => {
var RoleRoutesComponent: any = null;
switch (userRole) {
case "Admin":
RoleRoutesComponent = Loadable({
loader: () => import('./systemAdminRoutes/SystemAdminRoutes'),
loading: () => <LoadingPage />
});
break;
default:
break;
}
return (
<div>
<RoleRoutesComponent/>
</div>
);
}
export default Routes;
And then the routes component
const SystemAdminRoutes = () => {
var key = "/";
return (
<Switch>
<Route key={key} exact path="/" component={HomePage} />
<Route key={key} exact path="/home" component={HomePage} />
<Route key={key} path="/second" component={SecondPage} />
<Route key={key} path="/third" component={ThirdPage} />
...
<Route key={key} component={NotFoundPage} />
</Switch>
);
}
export default SystemAdminRoutes;
So the issue is whenever the user navigates from "/" to "/second" etc... app re-renders Routes, meaning the role switch logic is rerun, the user-specific routes are reloaded and re-rendered and state on pages is lost.
Things I've tried;
I've tried this with both react-loadable and React.lazy() and it has the same issue.
I've tried making the routes components classes
Giving all Routes down the tree the same key
Rendering all components down to the switch with path "/" but still the same problem.
Changing Route's component prop to render.
Changing the main app render method to component={Routes} and getting props via redux
There must be something wrong with the way I'm rendering the main routes component in the app component but I'm stumped, can anyone shed some light? Also note this has nothing to do with react-router's switch.
EDIT: I've modified one of my old test project to demonstrate this bug, you can clone the repo from https://github.com/Trackerchum/route-bug-demo - once the repo's cloned just run an npm install in root dir and npm start. I've got it logging to console when the Routes and SystemAdminRoutes are re-rendered/remounted
EDIT: I've opened an issue about this on GitHub, possible bug
Route re-rendering component on every path change, despite path of "/"
Found the reason this is happening straight from a developer (credit Tim Dorr). The route is re-rendering the component every time because it is an anonymous function. This happens twice down the tree, both in App and Routes (within Loadable function), below respectively.
<Route path="/" component={() => <Routes userRole={user.Role} />} />
needs to be
<Routes userRole={user.Role} />
and
loader: () => import('./systemAdminRoutes/SystemAdminRoutes')
Basically my whole approach needs to be rethought
EDIT: I eventually fixed this by using the render method on route:
<Route path="/" render={() => <Routes userRole={user.Role} />} />
Bumped into this problem and solved it like this:
In the component:
import {useParams} from "react-router-dom";
const {userRole: roleFromRoute} = useParams();
const [userRole, setUserRole] = useState(null);
useEffect(()=>{
setUserRole(roleFromRoute);
},[roleFromRoute]}
In the routes:
<Route path="/generic/:userRole" component={myComponent} />
This sets up a generic route with a parameter for the role.
In the component useParams picks up the changed parameter und the useEffect sets a state to trigger the render and whatever busines logic is needed.
},[userRole]);
Just put the "/" in the end and put the other routes above it.
Basically it's matching the first available option, so it matches "/" every time.
<Switch>
<Route key={key} exact path="/home" component={HomePage} />
<Route key={key} path="/second" component={SecondPage} />
<Route key={key} path="/third" component={ThirdPage} />
<Route key={key} exact path="/" component={HomePage} />
<Route key={key} component={NotFoundPage} />
</Switch>
OR
<Switch>
<Route path="/second" component={SecondPage} />
<Route exact path="/" component={HomePage} />
<Route path="*" component={NotFound} />
</Switch>
Reorder like this, it will start working.
Simple :)