How to create a condition depending on the route one is in? - reactjs

I have an app that is divided in 3 sections, a Header, Middle and Popular
<>
<Header />
<Middle />
<Popular />
</>
Header has a button that says 'Book it now' <Link to="/booking"> <button> Book it now </button> </Link>. When clicked, it uses React Router to switch route in the Middle component.
The 'Middle' component:
<Switch>
<Route path="/booking"> //When 'Book it now' is clicked, this is displayed.
<Booking />
</Route>
<Route path="/">
<Form />
</Route>
</Switch>
I would like to now make that Header button disappear when the route is '/booking'. I could create state and switch it to false after the button is clicked to not display the button, but I was thinking it would be pretty handy if I could do something like this in the Header file:
if (route === '/booking') {
//don't display button in Header (also, how would I do this?)
}

You can use withRouter imported from react-router-dom
import { withRouter } from 'react-router-dom';
const OtherComponent = withRouter(props => <MyComponent {...props}/>);
class MyComponent extends Component {
render() {
const { pathname } = this.props.location;
if(pathnanme != 'booking') {
<button>Hidden on booking</button>
}
}
}
That should be your Booking component:
import { withRouter } from 'react-router-dom';
const Booking = (props) => {
const { pathname } = props.location;
console.log(pathname);
return (
<div>
{(pathname !== 'booking' && pathname !== '/booking') ? <button>Hidden on booking</button> : null}
<span>always shown</span>
</div>
);
}
export default withRouter(Booking)
Hope this helps you

If you are using react-router v5 then I like to use hooks:
let location = useLocation();
let isBookingPage = location.pathname.includes('booking')
<>
{!isBookingPage && <Header />}
<Middle />
<Popular />
</>

By using the React Router 'useLocation' hook, you can have access to the route at which the user is currently at:
import { useLocation } from 'react-router-dom'
const location = useLocation() //with no arguments
console.log(location.pathname) //e.g. '/' or '/booking'
The location can the be used to conditionally render anything, in any component, depending on the route the user is at
location.pathname === '/booking' && <button> Book it now </button>

Related

Why browser back button returns a blank page with the previous URL

On pressing the browser back button why does an empty-blank page is displayed instead of the component that I'd visited before? Only the URL is getting changed to the previous one. Using React Router v5
That is really frustrating, how can I fix this ?
SignUp.js
render() {
return (
<div className='signUp-div'>
<Header />
<Router history={history}>
<div className='form-div'>
<Redirect to='/signup/mobile' /> // Default page
<Switch>
<Route exact path={'/signup/mobile'} component={MobileNum} />
<Route exact path={'/signup/idnumber'}>
<IdentNumber setPersonalID={this.props.setUserNumber} />
</Route>
<Route exact path={'/signup/password'}>
<CreatePass
setIfSignUp={this.props.setIfSignUp}
/>
</Route>
</Switch>
</div>
</Router>
</div>
);
}
IdentNumber.js
const IdentNumber = ({setPersonalID}) => {
const handleCheckID = () => {
history.push('/signup/password');
}
return (
<div className='form-div'>
<button
onChange={(event) => onChangeHandler(event)}
> Page password</button>
</div>
);
};
export default IdentNumber;
Did I explain it right ?
Thanks
From the code sandbox link, I've observed a few things that could potentially cause this issue.
Update your imports from import { Router } from "react-router-dom";
to
import { BrowserRouter as Router } from "react-router-dom";
<BrowserRouter> uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL.
The routes will remain the same. You're using react-router-dom v5.2.0, you could use useHistory to get the history object. useHistory simplified the process of making components route-aware.
With my changes: https://codesandbox.io/s/suspicious-pine-5uwoq
We don't need exact key for all routes other than the default "/" when it is enclosed in Switch and placed in the end. But exact matches /signup/mobile and /signup/* as same. Switch renders only one route and whichever route is matched first.
An example project for reference.
And if you want to handle the back button event yourself, follow the below examples.
In a function component, we can handle the back button press by listening to the history object.
import { useHistory } from 'react-router-dom';
const Test = () => {
const history = useHistory();
useEffect(() => {
return () => {
if (history.action === "POP") {
}
};
}, [history])
}
listen to history in useEffect to find out if the component is unmounted. history.listen lets us listen for changes to history.
Example:
import { useHistory } from 'react-router-dom';
const Test = () => {
const history = useHistory();
useEffect(() => {
return history.listen(location => {
if (history.action === 'POP') {
}
})
}, [])
}
react-router-dom now has Prompt,
import { Prompt } from "react-router-dom";
<Prompt
message={(location, action) => {
if (action === 'POP') {
// back button pressed
}
return location.pathname.startsWith("/test")
? true
: `Are you sure you want to go to ${location.pathname}?`
}}
/>

react-router#5 route `onEnter` method not calling, scrolltoview not working

according to my requirement, when a user click on
<Link to="/products/shoe#product9">Go to projects and focus id 9</Link> I would like to show him the product. (hello page) for that I do this:
import React from "react";
import { Link, Route, Switch, Redirect } from "react-router-dom";
import "./products.scss";
const Shoes = React.lazy(() => import("./shoes/shoes.component"));
const Cloths = React.lazy(() => import("./cloths/cloths.component"));
function hashScroll() {
alert("called");
const { hash } = window.location;
if (hash !== "") {
setTimeout(() => {
const id = hash.replace("#", "");
const element = document.getElementById(id);
if (element) element.scrollIntoView();
}, 0);
}
}
export default class Products extends React.Component {
render() {
return (
<div>
<header>
<Link to="/products/shoe">Shoes</Link>
<Link to="/products/cloths">Cloths</Link>
</header>
<h1>Products page</h1>
<main>
<Switch>
<Redirect exact from="/products" to="/products/shoe" />
<Route path="/products/shoe" onEnter={hashScroll}>
<Shoes />
</Route>
<Route path="/products/cloths">
<Cloths />
</Route>
</Switch>
</main>
</div>
);
}
}
I attached an onEnter function to call and scroll, so when there is a #hash let it scroll. It's not working at all. Please navigate to Hello page, from you click the link to go to products page.
Live Demo
onEnter is no longer working in react-router
What you can do is pass a prop to the component
<Shoes onEnter={hashScroll} />
inside the Shoes component execute it on componentDidMount.
componentDidMount = () => {
if (this.props.onEnter) {
this.props.onEnter();
}
};
demo

react-router with context - why doesn't it work the same with anchor vs Link

I have the below sample code using react-router and context hooks where I am trying to understand why it behaves differently when I use anchors instead of Link components. The Link components are commented out.
This app just simply displays a screen with an html link where you can click it to display component 2 (component 1 is displayed initially). I am updating the context value in the onClick event for the anchor (I use the setName function to update the name in the context).
When I use anchor tags, it doesn't keep the context value that was updated. So when it goes to component2, the name value in the context displays as person1. However, if I comment out the anchors and use the Link components instead, the context value is updated properly.
Why do the Link components work as expected but not the anchors when updating context?
import React, { useContext, useState } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
const NameContext = React.createContext();
function App() {
const [name, setName] = useState('name1');
return (
<NameContext.Provider value={{ name, setName }}>
<Router>
<Route exact path="/" component={Component1} />
<Route exact path="/component1" component={Component1} />
<Route exact path="/component2" component={Component2} />
</Router>
</NameContext.Provider>
);
}
function Component1() {
const { name, setName } = useContext(NameContext);
const history = useHistory();
return (
<>
<div>This is component 1, name = {name}</div>
<a href="/component2" onClick={() => setName('name2')}>
Click to display component 2
</a>
{/* <Link
onClick={() => setName('name2')}
to={(location) => {
return { ...location, pathname: '/component2' };
}}
>
Click to display component 2
</Link> */}
</>
);
}
function Component2() {
const { name, setName } = useContext(NameContext);
const history = useHistory();
return (
<>
<div>This is component 2, name = {name}</div>
<a href="/component1" onClick={() => setName('name3')}>
Click to display component 1
</a>
{/* <Link
onClick={() => setName('name3')}
to={(location) => {
return { ...location, pathname: '/component1' };
}}
>
Click to display component 1
</Link> */}
</>
);
}
export default App;
An anchor tag reloads the browser by default. If you want to avoid this default behavior you can call the preventDefault method on the onClick event.
react-router doesn't use anchor tags either, so if you want to use anchor tags you have to manually update the history.
<div>This is component 1, name = {name}</div>
<a
href="/component2"
onClick={(e) => {
e.preventDefault();
setName("name2");
history.push("/component2");
}}
>
Click to display component 2
</a>

Next JS, React have child component tell parent what to render

I am new to NextJS and React and want to create some dynamic routing. I have a parent page that is calling a child component which show some navigation.
In my parent component I have a layout that includes a header and right sidebar which will show on each page, but depending on the link that is clicked in my Nav component the body content will change.
My parent component:
import React from 'react';
import { Header } from './Header;
import { SubNav } from './SubNav;
import { Sidebar } from './Sidebar;
export const AboutPage = () => {
return (
<>
<Header />
<SubNav />
<div className="content">
>>>I want to do something like this <<<
{section === history && <History />}
{section === contact && <Contact />}
.....
</div>
<Sidebar />
</>
);
};
In my child Nav component I am mapping through some data that includes a 'value' for each different section that I have defined in an array.
<NavButtons
onClick={() => {
const path = AppRoute.ABOUT.replace(':section', 'value');
router.push(path, path, { shallow: true });
}}
/>
The page routes correctly with the value of the section changing each time I click on a different Navigation button but I can't get the content to change in the parent component, I have tried creating a function in the parent that will get the value from the child so that I can use it to define the sections in the parent, but it ends up creating an infinite loop. Confused on how to get the two to work together.
You can detect the pathname with the useRouter Hook.
Then you can check in a switch case statement which site the user is on and render the components for the route.
import { useRouter } from next/router
Inside the component
const router = useRouter();
The function to return the component
function renderSidebar(pathname){
switch(pathname){
...
}
Inside the render function
{ renderSidebar(router.pathname) }
See further usage in the NextJS docs
https://nextjs.org/docs/api-reference/next/router
import React from 'react';
import {useState} from 'react';
import { Header } from './Header';
import { SubNav } from './SubNav';
import { Sidebar } from './Sidebar';
export const AboutPage = () => {
const [section,setSection] = useState()
sectionHandler = (section) => {
setSection(section)
}
return (
<>
<Header />
<SubNav />
<div className="content">
>>>I want to do something like this <<<
{section === history && <History />}
{section === contact && <Contact />}
.....
</div>
<Sidebar />
</>
);
};
and in your child comp
<NavButtons
onClick={() => {
const path = AppRoute.ABOUT.replace(':section', 'value');
props.sectionHandler(section)
router.push(path, path, { shallow: true });
}}
/>
don't forget to pass sectionhandler from parent to child comp

React router Link not causing component to update within nested routes

This is driving me crazy. When I try to use React Router's Link within a nested route, the link updates in the browser but the view isn't changing. Yet if I refresh the page to the link, it does. Somehow, the component isn't updating when it should (or at least that's the goal).
Here's what my links look like (prev/next-item are really vars):
<Link to={'/portfolio/previous-item'}>
<button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
A hacky solution is to manaully call a forceUpate() like:
<Link onClick={this.forceUpdate} to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
That works, but causes a full page refresh, which I don't want and an error:
ReactComponent.js:85 Uncaught TypeError: Cannot read property 'enqueueForceUpdate' of undefined
I've searched high and low for an answer and the closest I could come is this: https://github.com/reactjs/react-router/issues/880. But it's old and I'm not using the pure render mixin.
Here are my relevant routes:
<Route component={App}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio" component={PortfolioDetail} >
<Route path="/portfolio/:slug" component={PortfolioItemDetail} />
</Route>
<Route path="*" component={NoMatch} />
</Route>
For whatever reason, calling Link is not causing the component to remount which needs to happen in order to fetch the content for the new view. It does call componentDidUpdate, and I'm sure I could check for a url slug change and then trigger my ajax call/view update there, but it seems like this shouldn't be needed.
EDIT (more of the relevant code):
PortfolioDetail.js
import React, {Component} from 'react';
import { browserHistory } from 'react-router'
import {connect} from 'react-redux';
import Loader from '../components/common/loader';
import PortfolioItemDetail from '../components/portfolio-detail/portfolioItemDetail';
import * as portfolioActions from '../actions/portfolio';
export default class PortfolioDetail extends Component {
static readyOnActions(dispatch, params) {
// this action fires when rendering on the server then again with each componentDidMount.
// but not firing with Link...
return Promise.all([
dispatch(portfolioActions.fetchPortfolioDetailIfNeeded(params.slug))
]);
}
componentDidMount() {
// react-router Link is not causing this event to fire
const {dispatch, params} = this.props;
PortfolioDetail.readyOnActions(dispatch, params);
}
componentWillUnmount() {
// react-router Link is not causing this event to fire
this.props.dispatch(portfolioActions.resetPortfolioDetail());
}
renderPortfolioItemDetail(browserHistory) {
const {DetailReadyState, item} = this.props.portfolio;
if (DetailReadyState === 'WORK_DETAIL_FETCHING') {
return <Loader />;
} else if (DetailReadyState === 'WORK_DETAIL_FETCHED') {
return <PortfolioItemDetail />; // used to have this as this.props.children when the route was nested
} else if (DetailReadyState === 'WORK_DETAIL_FETCH_FAILED') {
browserHistory.push('/not-found');
}
}
render() {
return (
<div id="interior-page">
{this.renderPortfolioItemDetail(browserHistory)}
</div>
);
}
}
function mapStateToProps(state) {
return {
portfolio: state.portfolio
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch: dispatch
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PortfolioDetail);
PortfolioItemDetail.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import Gallery from './gallery';
export default class PortfolioItemDetail extends React.Component {
makeGallery(gallery) {
if (gallery) {
return gallery
.split('|')
.map((image, i) => {
return <li key={i}><img src={'/images/portfolio/' + image} alt="" /></li>
})
}
}
render() {
const { item } = this.props.portfolio;
return (
<div className="portfolio-detail container-fluid">
<Gallery
makeGallery={this.makeGallery.bind(this)}
item={item}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
portfolio: state.portfolio
};
}
export default connect(mapStateToProps)(PortfolioItemDetail);
gallery.js
import React, { Component } from 'react';
import { Link } from 'react-router';
const Gallery = (props) => {
const {gallery, prev, next} = props.item;
const prevButton = prev ? <Link to={'/portfolio/' + prev}><button className="button button-xs">Previous</button></Link> : '';
const nextButton = next ? <Link to={'/portfolio/' + next}><button className="button button-xs">Next</button></Link> : '';
return (
<div>
<ul className="gallery">
{props.makeGallery(gallery)}
</ul>
<div className="next-prev-btns">
{prevButton}
{nextButton}
</div>
</div>
);
};
export default Gallery;
New routes, based on Anoop's suggestion:
<Route component={App}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio/:slug" component={PortfolioDetail} />
<Route path="*" component={NoMatch} />
</Route>
Could not get to the bottom of this, but I was able to achieve my goals with ComponentWillRecieveProps:
componentWillReceiveProps(nextProps){
if (nextProps.params.slug !== this.props.params.slug) {
const {dispatch, params} = nextProps;
PortfolioDetail.readyOnActions(dispatch, params, true);
}
}
In other words, for whatever reason when I use React Router Link to link to a page with the SAME PARENT COMPONENT, it doesn't fire componentWillUnMount/componentWillMount. So I'm having to manually trigger my actions. It does work as I expect whenever I link to Routes with a different parent component.
Maybe this is as designed, but it doesn't seem right and isn't intuitive. I've noticed that there are many similar questions on Stackoverflow about Link changing the url but not updating the page so I'm not the only one. If anyone has any insight on this I would still love to hear it!
It's good to share the components code also. However, I tried to recreate the same locally and is working fine for me. Below is the sample code,
import { Route, Link } from 'react-router';
import React from 'react';
import App from '../components/App';
const Home = ({ children }) => (
<div>
Hello There Team!!!
{children}
</div>
);
const PortfolioDetail = () => (
<div>
<Link to={'/portfolio/previous-item'}>
<button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
</div>
);
const PortfolioItemDetail = () => (
<div>PortfolioItemDetail</div>
);
const NoMatch = () => (
<div>404</div>
);
module.exports = (
<Route path="/" component={Home}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio" component={PortfolioDetail} />
<Route path="/portfolio/:slug" component={PortfolioItemDetail} />
<Route path="*" component={NoMatch} />
</Route>
);
componentWillReceiveProps is the answer to this one, but it's a little annoying. I wrote a BaseController "concept" which sets a state action on route changes EVEN though the route's component is the same. So imagine your routes look like this:
<Route path="test" name="test" component={TestController} />
<Route path="test/edit(/:id)" name="test" component={TestController} />
<Route path="test/anything" name="test" component={TestController} />
So then a BaseController would check the route update:
import React from "react";
/**
* conceptual experiment
* to adapt a controller/action sort of approach
*/
export default class BaseController extends React.Component {
/**
* setState function as a call back to be set from
* every inheriting instance
*
* #param setStateCallback
*/
init(setStateCallback) {
this.setStateCall = setStateCallback
this.setStateCall({action: this.getActionFromPath(this.props.location.pathname)})
}
componentWillReceiveProps(nextProps) {
if (nextProps.location.pathname != this.props.location.pathname) {
this.setStateCall({action: this.getActionFromPath(nextProps.location.pathname)})
}
}
getActionFromPath(path) {
let split = path.split('/')
if(split.length == 3 && split[2].length > 0) {
return split[2]
} else {
return 'index'
}
}
render() {
return null
}
}
You can then inherit from that one:
import React from "react";
import BaseController from './BaseController'
export default class TestController extends BaseController {
componentWillMount() {
/**
* convention is to call init to
* pass the setState function
*/
this.init(this.setState)
}
componentDidUpdate(){
/**
* state change due to route change
*/
console.log(this.state)
}
getContent(){
switch(this.state.action) {
case 'index':
return <span> Index action </span>
case 'anything':
return <span>Anything action route</span>
case 'edit':
return <span>Edit action route</span>
default:
return <span>404 I guess</span>
}
}
render() {
return (<div>
<h1>Test page</h1>
<p>
{this.getContent()}
</p>
</div>)
}
}
I got stuck on this also in React 16.
My solution was as follows:
componentWillMount() {
const { id } = this.props.match.params;
this.props.fetchCategory(id); // Fetch data and set state
}
componentWillReceiveProps(nextProps) {
const { id } = nextProps.match.params;
const { category } = nextProps;
if(!category) {
this.props.fetchCategory(id); // Fetch data and set state
}
}
I am using redux to manage state but the concept is the same I think.
Set the state as per normal on the WillMount method and when the WillReceiveProps is called you can check if the state has been updated if it hasn't you can recall the method that sets your state, this should re-render your component.
I am uncertain whether it fixes the original problem, but I had a similar issue which was resolved by passing in the function callback () => this.forceUpdate() instead of this.forceUpdate.
Since no one else is mentioning it, I see that you are using onClick={this.forceUpdate}, and would try onClick={() => this.forceUpdate()}.
Try to import BrowserRouter instead of Router
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom;
It worked for me after spending a couple of hours solving this issue.
I solved this by building '' custom component instead of '', and inside it I use in the method instead of :
import * as React from "react";
import {Navigate} from "react-router-dom";
import {useState} from "react";
export function ReactLink(props) {
const [navigate, setNavigate] = useState(<span/>);
return (
<div style={{cursor: "pointer"}}
onClick={() => setNavigate(<Navigate to={props.to}/>)}>
{navigate}
{props.children}
</div>
}

Resources