React: Hide a Component on a specific Route - reactjs

New to React:
I have a <Header /> Component that I want to hide only when the user visit a specific page.
The way I designed my app so far is that the <Header /> Component is not re-rendered when navigating, only the page content is, so it gives a really smooth experience.
I tried to re-render the header for every route, that would make it easy to hide, but I get that ugly re-rendering glitch each time I navigate.
So basically, is there a way to re-render a component only when going in and out of a specific route ?
If not, what would be the best practice to achieve this goal ?
App.js:
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
Main.js:
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route exact activeClassName="active" path="/art" component={Art} />
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);

I'm new to React too, but came across this problem. A react-router based alternative to the accepted answer would be to use withRouter, which wraps the component you want to hide and provides it with location prop (amongst others).
import { withRouter } from 'react-router-dom';
const ComponentToHide = (props) => {
const { location } = props;
if (location.pathname.match(/routeOnWhichToHideIt/)){
return null;
}
return (
<ComponentToHideContent/>
)
}
const ComponentThatHides = withRouter(ComponentToHide);
Note though this caveat from the docs:
withRouter does not subscribe to location changes like React Redux’s
connect does for state changes. Instead, re-renders after location
changes propagate out from the component. This means that
withRouter does not re-render on route transitions unless its parent
component re-renders.
This caveat not withstanding, this approach seems to work for me for a very similar use case to the OP's.

Since React Router 5.1 there is the hook useLocation, which lets you easily access the current location.
import { useLocation } from 'react-router-dom'
function HeaderView() {
let location = useLocation();
console.log(location.pathname);
return <span>Path : {location.pathname}</span>
}

You could add it to all routes (by declaring a non exact path) and hide it in your specific path:
<Route path='/' component={Header} /> // note, no exact={true}
then in Header render method:
render() {
const {match: {url}} = this.props;
if(url.startWith('/your-no-header-path') {
return null;
} else {
// your existing render login
}
}

You can rely on state to do the re-rendering.
If you navigate from route shouldHide then this.setState({ hide: true })
You can wrap your <Header> in the render with a conditional:
{
!this.state.hide &&
<Header>
}
Or you can use a function:
_header = () => {
const { hide } = this.state
if (hide) return null
return (
<Header />
)
}
And in the render method:
{this._header()}
I haven't tried react-router, but something like this might work:
class App extends Component {
constructor(props) {
super(props)
this.state = {
hide: false
}
}
toggleHeader = () => {
const { hide } = this.state
this.setState({ hide: !hide })
}
render() {
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route
exact
activeClassName="active"
path="/art"
render={(props) => <Art toggleHeader={this.toggleHeader} />}
/>
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
And you need to manually call the function inside Art:
this.props.hideHeader()

{location.pathname !== '/page-you-dont-want' && <YourComponent />}
This will check the path name if it is NOT page that you DO NOT want the component to appear, it will NOT display it, otherwise is WILL display it.

Related

<Outlet /> fails to rerender with react router v6

In the following code, the url changes but the content doesn't rerender until manual refresh. What am I doing wrong here? I could use props.children or something but don't really want to. My understanding of is that it should render the content of the nested elements under .
const LandingPage = () => {
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet />
</div>
)
}
export default class Router extends Component<any> {
state = {
teams: [team1, team2, team3] as Team[]
}
public render() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<LandingPage />} >
{
this.state.teams.map(team => {
const path = `/${team.name.toLowerCase()}`
return (
<Route path={path} element={
<BaseTeam
name={team.name}
TL={team.TL}
location={team.location}
members={team.members}
iconPath={team.iconPath}
/>
} />)
})
}
</Route>
</Routes>
</BrowserRouter>
)
}
}
Maybe signal to react library that a key has changed so that it needs to rerender the outlet
const LandingPage = () => {
const location = useLocation(); // react-router-dom
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet key={location.pathname}/>
</div>
)}
It seems the mapped routes are missing a React key. Add key={path} so each route is rendering a different instance of BaseTeam.
The main issue is that the BaseTeam component is the same "instance" for all the routes rendering it.
It should either also have a key prop specified so when the key changes BaseTeam is remounted and sets the name class property.
Example:
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path} // <-- add missing React key
path={path}
element={(
<BaseTeam
key={path} // <-- add key to trigger remounting
name={team.name}
/>
)}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
Or BaseTeam needs to be updated to react to the name prop updating. Use the componentDidUpdate lifecycle method to check the name prop against the current state, enqueue a state update is necessary.
Example:
class BaseTeam extends React.Component {
state = {
name: this.props.name
};
componentDidUpdate(prevProps) {
if (prevProps.name !== this.props.name) {
this.setState({ name: this.props.name });
}
}
render() {
return <div>{this.state.name}</div>;
}
}
...
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path}
path={path}
element={<BaseTeam name={team.name} />}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
As you've found out in your code though, just rendering the props.name prop directly is actually the correct solution. It's a React anti-pattern to store passed props into local state. As you can see, it requires extra code to keep the props and state synchrononized.

How hide component from another component start render through route on react

I would like to hide a component when another component is routing
for more specific, i have a fixed bottom nav that i want to hide when a component is been route by user, in this case its a comment box
My first option was trying on the parent component with browserhistory and history.listen, and componning a string with math.params for get a match between, and this change the state of parent, witch will hide the bottomnav, and the code wrote was trying through a cyclelife passing props, but nothing, anyone can help me please?
class App extends React.Component {
constructor(props){
super(props);
this.state = {
showBottomNav: true
}
this.hideBottomNav = this.hideBottomNav.bind(this)
}
hideBottomNav= () => {
this.setState({
showBottomNav: false
})
}
render() {
return (
<Router>
<NavBar />
<Switch>
<Route path='/' exact component={Home} />
<Route path='/wars' exact component={Tournament} />
<Route path='/shop' exact component={Shop} />
<Route path='/library' exact component={Library} />
<Route
path='/:id'
exact
render={ props => <ExpandedPost {...props} parentMethod={() => this.hideBottomNav()} />}
/>
</Switch>
<BottomNav />
</Router>
);
}
}
export default App;
And the children Component
export default function PostReview(props) {
const classes = useStyles();
useEffect(() => {
props.parentMethod()
},[props])
return (
<div>....

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 Router v4 and React Transition Group v2

I am trying to find a working example of low level animations using the React Router v4 with React Transition Group v2. I have looked at the example on the React Router docs but they only use CSS Animation with one route.
This is how I currently use the React Router:
export const App = () => (
<div className="app-container">
<main className="app-container__content">
<Switch>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</main>
</div>
);
And this is my Root.jsx:
const Root = ({ store, history }) => (
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/" component={App} />
</Switch>
</ConnectedRouter>
</Provider>
);
I have tested the solution here: https://hackernoon.com/animated-page-transitions-with-react-router-4-reacttransitiongroup-and-animated-1ca17bd97a1a - but it doesn't work. Could some one point me in the right direction?
Update
I have tried like this, but the callback doesn't get called. So I end up with to pages in the dom.
export const App = ({ location }) => {
console.log(location);
return (
<div className="app-container">
<main className="app-container__content">
<ScrollToTop />
<TransitionGroup>
<Switch key={location.pathname} location={location}>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</TransitionGroup>
</main>
</div>
);
};
you are missing the Transition component itself
it should look something like this:
<TransitionGroup>
<CSSTransition
key={location.key}
classNames="page-animation"
timeout={{ enter: PAGE_ENTER_ANIMATION_SPEED, exit: PAGE_EXIT_ANIMATION_SPEED }}>
<Switch location={location}>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</CSSTransition>
</TransitionGroup>
notice that the key is on the CSSTransition itself and not on the Switch
Update
If you want to implement it by yourself
here is a basic implementation of CSSTransition
class MyTransition extends React.Component {
constructor(props) {
super(props);
this.state = {
inTransition: true,
playAnimation: false,
};
}
componentDidMount() {
setTimeout(() => {
this.setState({playAnimation: true});
}, 1);
this.removeClassesAndFireOnExited();
}
removeClassesAndFireOnExited() {
setTimeout(() => {
this.setState({playAnimation: false, inTransition: false}, () => {
if (!this.props.in) {
this.props.onExited(this);
}
});
}, this.props.timeout[this.props.in ? 'enter' : 'exit']);
}
componentWillReceiveProps(nextProps) {
if (nextProps.in !== this.props.in) {
this.setState({inTransition: true});
setTimeout(() => {
this.setState({playAnimation: true});
}, 1);
this.removeClassesAndFireOnExited();
}
}
render() {
const baseClassName = this.props.in ? 'page-animation-enter' : 'page-animation-exit';
const {inTransition, playAnimation} = this.state;
const transitionClasses = [...(inTransition ? [baseClassName] : []), ...(playAnimation ? [baseClassName + '-active'] : [])];
return (
<div className={transitionClasses.join(' ')}>
{this.props.children}
</div>
);
}
}
you get this.props.in from TransitionGroup to indicate whether you are entering or leaving.
this.props.onExited is another prop that you get from TransitionGroup, you must call it when the exit animation completes, so TransitionGroup will know that you should be unmounted.
Here is an article explaining how to setup react-router v4 and react-transition-group with multiple routes and with transitions depending of the next state: https://medium.com/lalilo/dynamic-transitions-with-react-router-and-react-transition-group-69ab795815c9

Link to= not working with /route:parameters

I have react router setup with route parameters :name
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/about/:name" component={About} />
<Route component={NotFound} />
</Switch>
</Router>
Using <Link to=, links from Index correctly route to eg /about/vinnie.
However, if the <Link to= component is on the About page, clicking the link merely updates the browser URL, but dies not re-render the correct page.
Any clues why this might be happening?
About.jsx
render () {
const id = this.props.match.params.name;
return (
<div className="Explore">
<div className="container">
<Api endpoint={[this._apiEndpoint(id, 'info')]} loadData={true}>
<CustomerInfo />
</Api>
...
Api.jsx
render () {
let showData = this.props.loadData ? null : <button onClick={() => this._loadButton()}>Show</button>;
return (
<span>
{this.props.children && React.cloneElement(this.props.children, {
apiData: this.state.rawData
})}
{showData}
</span>
);
};
CustomerInfo.jsx
class CustomerInfo extends React.Component {
constructor(props) {
super(props);
this.state = {
CustomerInfo: {}
}
}
componentWillReceiveProps(nextProps) {
if (this.props.apiData !== nextProps.apiData) {
this.setState({CustomerInfo: nextProps.apiData[0]});
}
}
render() {
...
I think you need to add the exact prop to your first <Route />. Otherwise your routes match Index and About and I think react-router intentionally renders both.

Resources