Active NavLink to parent element - reactjs

I'm using React Router v4 and I have a case where on my navigation links, I want to enable the active className to the NavLink parent element, not the NavLink itself.
Is there a way to access the path (match) even though I'm not inside the Switch element?
Or do I have to keep state? Because I'm feeling it's kinda missing the idea of router.
Here's my example, I want to apply the active className to li element not NavLink:
const {
HashRouter,
Switch,
Route,
Link,
NavLink,
} = ReactRouterDOM
const About = () => (
<article>
My name is Moshe and I'm learning React and React Router v4.
</article>
);
const Page = () => (
<Switch>
<Route exact path='/' render={() => <h1>Welcome!</h1>} />
<Route path='/about' component={About}/>
</Switch>
);
const Nav = () => (
<nav>
<ul>
<li><NavLink exact to="/">Home</NavLink></li>
<li><NavLink to="/about">About</NavLink></li>
</ul>
</nav>
);
class App extends React.Component {
render() {
return (
<div>
<Nav />
<Page />
</div>
);
}
}
ReactDOM.render((
<HashRouter>
<App />
</HashRouter>),
document.querySelector("#app"));
https://codepen.io/moshem/pen/ypzmQX

It doesn't seem like it is very easy to achieve. I used withRouter HOC described in react router docs. It gives access to { match, location, history } from props inside components located outside of Routess. In the example I wrapped Nav component to get location and its pathname. Here is the example code:
class Nav extends React.Component {
getNavLinkClass = (path) => {
return this.props.location.pathname === path ? 'active' : '';
}
render() {
return (
<nav>
<ul>
<li className={this.getNavLinkClass("/")}><NavLink exact to="/">Home</NavLink></li>
<li className={this.getNavLinkClass("/about")}><NavLink to="/about">About</NavLink></li>
</ul>
</nav>
)};
}
Nav = withRouter(Nav);
You will probably have to take care of params in your routes (if you have any), to match properly. But you still have to match for each path you have in your NavLink, which might not be pretty code. But the idea is that when the route is changed, Nav is rerendered and correct li is highlighted.
Here is a working example on codesandbox.

Can be achived with Route component
<ul>
<Route path="/about">
{({ match }) => <li className={match ? 'active' : undefined}><Link to="/about">About</Link></li>
</Route>
</ul>
Reference: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md#children-func

If you abandon the NavLink components altogether, you can create your own components that emulate the "activeness" of a NavLink by using useHistory() and useLocation() from react-router-dom.
Dashboard.js
const routeItems = [
{ route: '/route1', text: 'Route 1' },
{ route: '/route2', text: 'Route 2' },
];
<Router>
<NavBar routeItems={routeItems} />
</Router>
In NavBar.js, we just need to check to see if the current active route is the same as the route for any individual item on the
NavBar.js
import { useHistory, useLocation } from 'react-router-dom';
const NavBar = (props) => {
const { routeItems } = props;
const history = useHistory();
const location = useLocation();
const navItems = routeItems.map((navItem) => {
return (
<div style={{
backgroundColor: navItem.route === location.pathname ? '#ADD8E6' : '',
}}
onClick={() => {
history.push(navItem.route);
}}
>
{navItem.text}
</div>
);
});
return (navItems);
};
export default NavBar;

I found simpler solution for my case I have nested items but I know the base of each nest
for example the base of nest is /customer it contains items like so
/customer/list , /customer/roles ...
So did put some logic in isActive prop in the parent NavLink
code with explanation down :
<NavLink
to={item.route}
activeClassName={classes.activeItem}
onClick={e => handleItemClick(e, key)}
isActive={(match, location) => {
// remove last part of path ( admin/customer/list becomes admin/customer for example )
const pathWithoutLastPart = location.pathname.slice(0, location.pathname.lastIndexOf("/"));
// if current parent is matched and doesn't contain childs activate it
if (item.items.length === 0 && match) {
return true;
}
// if sliced path matches parent path
in case of customer item it becomes true ( admin/customer === admin/customer )
else if (pathWithoutLastPart === item.route) {
return true;
}
// else inactive item
else {
return false;
}
}}
>
...
</NavLink>
Now parent active with his child

Related

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>

How to dynamically create a React component based on url param?

I'd like my React component to generate based on the url param, in this case, the :id param. I'm struggling rendering the component. I have the different Components defined in separate files (ex. Loader.js, Radio button.js, Accordion menu.js).
Here's my (reduced for clarity) code that is continuously failing :)
import React from 'react';
import { Switch, Link, Route } from 'react-router-dom';
import Grid from '../Components/Grid'
function Overview () {
const components = [
{id: 'accordion-menu',
name: 'Accordion menu'},
{id: 'radio-button',
name: 'Radio button'},
{id: 'loader',
name: 'Loader'},
]
const componentPage = ({match}) => {
const findId = components.find((el) => {
match.params.id = el.id;
return findId.name;
}
)}
return (
<Router>
<div className="components">
<h3>Components</h3>
<p>This header and the menu will always appear on this page!</p>
<menu>
{components.map(({id, name}) => (
<li>
<Link to={`/components/${id}`}>{name}</Link>
</li>
))}
</menu>
<Switch>
<Route exact path={'/components/'} component={Grid}/>
<Route path={'/components/:id'} component={componentPage}/>
</Switch>
</div>
</Router>
)
}
export default Overview;
const componentPage = ({match}) => {
const findId = components.find((el) => {
match.params.id = el.id;
return findId.name;
}
)}
I think what you mean here is to return el.name not findId.name.
the other thing is you are trying to display the function as a component which won't work.

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

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>

How to achieve Dynamic routing in React Router 4?

I have a list of articles like this:
<div>
{
this.props.articles.map(article => {
return (
<ArticleCard key={article._id} article={article} />
)
})
}
</div>
In the ArticleCard component, I'm only showing the title of my article. I want to put a link to it which would create new URL like 'article-title' and show the content.
How to achieve this?
In your ArticleCard, you have to create a Link that will route to your full Article. This link will include the id of the article you are trying to render (ex. articles/${article._id})
By writing the Route path of the component Article as articles/:id, this will allow us to catch that id when Article is rendered (accessible via this.props.match.params.id)
Then, assuming that id is used to fetch the article from some other API, a good place to call that would be the componentDidMount of your Article component.
Here is a small example which may help you out:
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Switch
} from 'react-router-dom'
const ParamsExample = () => (
<Router>
<Switch>
<Route exact path="/" component={ArticleList} />
<Route path="/articles/:id" component={Article} />
</Switch>
</Router>
)
const article = {
_id: 1,
title: 'First Article'
};
const ArticleList = () => (
<div>
<ArticleCard key={article._id} article={article} />
</div>
);
const ArticleCard = ({ article }) => (
<div>
<h2>{article.title}</h2>
<Link to={`/articles/${article._id}`}>SEE MORE</Link>
</div>
);
class Article extends React.Component {
componentDidMount() {
console.log('Fetch API here: ', this.props.match.params.id);
}
render() {
return (
<div>
{`Fetching...${this.props.match.params.id}`}
</div>
);
}
}
export default ParamsExample

Resources