Add two HOC component in <Switch/> not work - reactjs

I try to put 2 HOC in switch but, only routers in first be called, the second is not called.
// if user is not login, show login page, otherwise add a side bar to children and show up
#inject("userStore")
#observer
class Auth extends React.Component {
render() {
let { userStore, children } = this.props;
return userStore.isLogin ? <CoreLayout>{children}</CoreLayout> : <Login />;
}
}
// if user is not login, show login page, otherwise show children
#inject("userStore")
#observer
class AuthWithoutLayout extends React.Component {
render() {
let { userStore, children } = this.props;
return userStore.isLogin ? children : <Login />;
}
}
export { Auth, AuthWithoutLayout };
And the Switch part:
<ConfigProvider locale={locale}>
<Switch>
<Route exact path="/" component={Login} />
<AuthWithoutLayout>
<Route path="/switch-role" component={SwitchRole} />
</AuthWithoutLayout>
<Auth>
<Route path="/user-list" component={UserList} />
</Auth>
</Switch>
</ConfigProvider>
If I input /localhost:3000/switch-role to browser, child page can show up correctly, but if I input /localhost:3000/user-list, I see a black page. if I remove AuthWithoutLayout part, the user-list page will show up.
Please help.

Switch
Renders the first child <Route> or <Redirect> that matches the location.
BTW, neither of those are Higher Order Components, but rather they are simple wrapper components. You can correct your Auth component, but your AuthWithoutLayout is a layout container and better suited to decorate anything other than a route or redirect.
Basically in your "auth" component you want to check some authentication condition and if authenticated render the Route, otherwise you redirect the user where you want them, usually the login path.
Your containers should also apply the Single Responsibility Principle, meaning an auth container should only concern itself with authentication, and a layout container should only concern itself with content layout.
Here's a sample auth route rewrite
// if user is logged in, render Route, otherwise Redirect to login "/"
#inject("userStore")
#observer
class AuthRoute extends Component {
render() {
const { userStore, ...props } = this.props;
return userStore.isLogin ? <Route {...props} : <Redirect to="/" />;
}
}
Usage:
<ConfigProvider locale={locale}>
<Switch>
<Route exact path="/" component={Login} />
<AuthRoute path="/switch-role" component={SwitchRole} />
<AuthRoute path="/user-list" component={UserList} /> // <-- use a layout container to decorate UserList!
</Switch>
</ConfigProvider>

The problem with above code is that Switch renders the First match component. So when you render AuthWithoutLayout without a Route, it assumes that this is the component that needs to be rendered and will not check any further and hence Auth is ignored
The solution is to write AuthWithoutLayout and Auth both with Routes
<ConfigProvider locale={locale}>
<Switch>
<Route exact path="/" component={Login} />
<Route path="/switch-role">
<AuthWithoutLayout>
<SwitchRole />
</AuthWithoutLayout>
</Route>
<Route path="/user-list">
<Auth>
<UserList />
</Auth>
</Route>
</Switch>
</ConfigProvider>

Related

How to redirect to log in page after click logout button on navbar header in React?

I'm new to React. I have react router config in App.js like this:
<BrowserRouter>
<div className="App">
<Header />
<Switch>
<Route exact path="/" component={Home}>
</Route>
<Route exact path="/management" component={Management}>
</Route>
<Route exact path="/sign-up" component={SignUpForm}>
</Route>
<Route exact path="/sign-in" component={SignInForm}>
</Route>
<Route component={Error}>
</Route>
</Switch>
</div>
</BrowserRouter >
I want header to show in every page, there's a button of logout at header, I want to redirect to /sign-in page after I click it. In my header component it's like this:
class Header extends Component {
constructor(props) {
super(props);
this.state = {
redirect: false
}
}
logout = () => {
sessionStorage.setItem("userToken", '');
sessionStorage.clear();
this.setState({ redirect: true });
}
render() {
if (this.state.redirect) {
return (
<Redirect to={'/sign-in'} />
)
}
return (
<div>
<Navbar collapseOnSelect expand="md" bg="dark" variant="dark" fixed="top" >
......
<NavLink to="/management" className="header-link"><FontAwesomeIcon icon="cog" size="lg" /></NavLink>
<button type='button' onClick={this.logout}>Log Out</button>
</Nav>
</Navbar.Collapse>
</Navbar>
</div>
);
}
}
export default Header;
There will be errors "Warning: You tried to redirect to the same route you're currently on: "/sign-in", and the nav bar will disappear only the body of sign-in shows. May I know what is the correct way to do this? I also tried this.props.history.push('/sign-in') but there's no props.history, probably because header is not in route? Should i use with Router? Or should I actually just make every page import header instead put it in app.js? or what is actually the right way to do this? Thank you so much for your help!
You can implement login/logout with route using HOC that checks the session item with every route change. If the session has userToken then it will redirect to given component otherwise will redirect to login component.
import React from "react"
import {Redirect} from "react-router-dom"
export const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
sessionStorage.getItem('userToken') ? <Component {...props} /> : <Redirect to="/sign-in"/>
)} />
)
import <PrivateRoute> and use it as the authorized path. And keep all the other path as normal routes in which you don't want authorization.
<BrowserRouter>
<div className="App">
<Header />
<Switch>
<PrivateRoute path="/" component={Home} />
<PrivateRoute path="/management" component={Management} />
<Route path="/sign-up" component={SignUpForm} />
<Route path="/sign-in" component={SignInForm} />
<Route component={Error} />
</Switch>
</div>
</BrowserRouter >
So while you do log out, the session item will be removed and automatically redirect to sign-in page.
class Header extends Component {
....
logout = () => {
sessionStorage.removeItem("userToken");
sessionStorage.clear();
}
render() {
return (
<div>
...
<button type='button' onClick={this.logout}>Log Out</button>
</div>
)
}
}
export default Header;
Components tied up to Routes gets access to history object as prop so you can mutate it as you need, such as logging out. Since your Header component doesn't have access to the history object, you will have to use a lower level router to give it access to the history object:
import { Router } from "react-router"
import { createBrowserHistory } from "history"
const history = createBrowserHistory()
<Router history={history}>
<div className="App">
<Header history={history} />
<Switch>
<Route exact path="/" component={Home}>
</Route>
<Route exact path="/management" component={Management}>
</Route>
<Route exact path="/sign-up" component={SignUpForm}>
</Route>
<Route exact path="/sign-in" component={SignInForm}>
</Route>
<Route component={Error}>
</Route>
</Switch>
</div>
</Router>
now inside you Header component you can call history.push('/login')
see reference: https://reacttraining.com/react-router/web/api/Router/history-object
There are two approaches you're mentioning here. You can use the higher order component 'withRouter' that gives components access to the history object. By using the history object that would get passed to your component as a prop, you can push to the route you want.
Personally, I like setting up my signout links to render a component that houses the log-out logic and renders a redirect to log-in once it's complete. That way users can go directly to the sign-out link if they want, and you can link to it from anywhere in your app as needed, without having to duplicate the logic.
In your browser router, you can add a path for "/logout" that renders a component like this (based on your logic):
import React, { Component } from 'react';
import { Redirect } from 'react-router';
export default class LogOut extends Component {
state = {
redirect: false,
};
componentDidMount() {
sessionStorage.setItem("userToken", '');
sessionStorage.clear();
this.setState({ redirect: true });
}
render() {
return this.state.redirect ?
<Redirect to={'/sign-in'} /> :
null;
}
}
Normally I would make an ajax request to clear a session and then setState once that's complete, but yours is all server-side.

(React) Render only one child (from props.children) depends of route (location.pathname)

Here is my Switch:
<Switch>
<Main>
<Route exact path="/login" component={LoginForm} />
<Route exact path="/register" component={RegisterForm} />
</Main>
</Switch>
The Main Component - is a Wrapper of the Children:
class Main extends Component {
render() {
const { children } = this.props;
return (
...
{children}
...
)
}
}
This return me all my (children) components as LoginForm as RegisterForm
So I need to get only one component depends of the route (props.location.pathname)
What is the right solution in that case?
Seems you need to put 'exact' for the first route tag also. It will solve the problem. As the '/' will get match to all the routes. So to make it specific, you need to put exact for first route as well.

How to save bad invalid URLs that were typed in?

I am having a react-redux app and react-router v4 inside of app
Is there a way to catch all invalid URLs that were entered and save them to an array, like so ['https://mysite.co/progects', 'https://mysite.co/sometypo', 'https://mysite.co/something']?
And then I want to send that data to server for building some redirects and some sitemap
Currently I have this:
<Switch>
{/* <Route path='/blog' exact component={Blog} /> */}
<Route path='/projects/:id' component={ProjectDetails} />
<Route path='/career/:id' component={CareerDetails} />
<Route path='/apply-for-job' render={(props) => (
<ModalWindow
{...props}
modalHeader='Apply form'>
<ApplyForm history={props.history} />
</ModalWindow>
)} />
<Route exact path='/' component={withScrollPreservation(LandingPage)} />
<Route component={NoMatch} />
{/* <Route component={withScrollPreservation(LandingPage)} /> */}
</Switch>
In your NoMatch component, you can have the logic to update unmatched/incorrect urls
class NoMatch extends React.Component {
componentDidMount() {
const { addNoMatchUrl } = this.props;
// you might want to handle the duplicate url logic here too in addNoMatchUrl method
addNoMatchUrl(this.props.location.pathname);
}
componentDidUpdate(prevProps) {
const { location, addNoMatchUrl } = this.props;
if (location.pathname !== prevProps.location.pathname) {
addNoMatchUrl(location.pathname);
}
}
render() {
// render logic
}
}
export default connect(null, {addNoMatchUrl});
If you want, say, to redirect someone, who typed '/progects' to '/projects' - well, that's nice UX, but your Switch block will be cluttered with tens of possible invalid urls.
As I see it, maybe you should add <Redirect to='/main' /> at the bottom of your Switch so any invalid url gets redirected to Main component (or whichever you have) or to 404-Component.
If you still want to gather them, then instead of redirecting to Main or 404 Component, send them to specific Error component, where you can get the link via this.props.history.location and handle that link further in the component: send to server, set that url in local/session storage, etc.
Note that you'll need a way to store that data someplace which won't get cleared on unmounting.
In order to send users to that route you'll need to place that at the bottom of your Switch.
<Switch>
...{your routes}
<Route component={Error} />
</Switch>
So actually all you need to do is handle those urls in your NoMatch component, like so
const path = this.props.history.location;
axios.post('your api', path);

Postponing component mounting unless I return true

I have a controller called PersonCreate. In this component, I have to check the role of the authenticated user (this.props.app.session.user.role.isAdmin()) and depending on the value, render some form.
The problem is, it takes a while to generate the session object. When I browse the endpoint for http://.../person/create, it tries to call this.props.app.session.user.role.isAdmin(), which throws null pointer exception because session is not generated yet.
My Router file looks like this.
class RootComponent extends React.Component<any, any> {
private generateSession() {
store.dispatch(SessionActions.generate());
}
public render() {
return (
<Router history={browserHistory}>
<Route path="/" component={Layout}>
<Route path="app" onEnter={this.generateSession}>
<Route path="person">
<Route path="create" component={PersonCreate} />
<Route path="update/:id" component={PersonUpdate} />
<Route path="delete/:id" component={PersonDelete} />
...
Basically store.dispatch(SessionActions.generate()) generated a Saga which does a series of asyncronous stuff. (e.g validate token, obtain session information, refresh local storage etc.) I have to start rendering components after they complete.
Any ideas?
You can use check in RootComponent like this:
public render() {
if (!this.props.app.session) {
return null;
}
and it will render routes only after session will be initialized.
Or you can use similar check in PersonCreate component.
You can create a React Component that contains login for rendering. This component wraps all of the routes that require users information.
<Route path=”/” component={App}>
<Route path=”cart” component={Cart}/>
<Route path=”login” component={Login}/>
<Route component={EnsureLoggedInContainer}>
<Route path=”checkout” component={Checkout}/>
<Route path=”account” component={Account}/>
</Route>
</Route>
And in this "parent"(EnsureLoggedInContainer) component in render do something like:
render() {
if (isLoggedIn) {
return <AppropriateUserForm />;
} else {
return <LoadingIndicator />;
}
}
You can read more about this approach here:
https://medium.com/the-many/adding-login-and-authentication-sections-to-your-react-or-react-native-app-7767fd251bd1

React router not rendering inactive route components

I started with create-react-app and am attempting to set up auth-based routing via a Gatekeeper component. Right now the only thing the Gatekeeper component does is render its children. I use redux to get the current user from the state tree and save it as currentUser. I'll use that to validate access to the child routes later.
import React from 'react';
import { connect } from 'react-redux';
import { subscribeToCurrentUser } from '../../reducers/authentication';
class Gatekeeper extends React.Component {
componentDidMount() {
this.props.subscribeToCurrentUser();
}
render() {
return this.props.children
}
}
function mapStateToProps(state) {
return {
currentUser: state.currentUser
}
}
const GatekeeperContainer = connect(mapStateToProps, {subscribeToCurrentUser})(Gatekeeper);
export default GatekeeperContainer;
If I initially load the app on, say /account everything loads as expected. But if I navigate to /templates/123 via a <NavLink> the URL changes but the <Template> component doesn't render. Inspecting with React Dev Tools shows me the children of every route underneath the Gatekeeper component is null. If I refresh the page then the <Template> component renders as expected but navigating back to /account doesn't render the <AccountPage> component.
<Provider store={store}>
<Router history={history}>
<div>
<Route exact path="/" component={LandingPage} />
<Layout>
<Route path="/login" component={Login} />
<Gatekeeper>
<Switch>
<Route path="/home" component={HomePage} />
<Route path="/templates/:templateID" component={Template} />
<Route path="/account" component={AccountPage} />
</Switch>
</Gatekeeper>
</Layout>
</div>
</Router>
</Provider>
What am I doing wrong here?

Resources