React-router: activeClassName to NavLink properly v4

Created on 5 Mar 2017  Â·  33Comments  Â·  Source: ReactTraining/react-router

v4 build 0.7 seems to have had issues on my NavLinks, where the activeClassName is not updating when the url changes.

This simply makes the navigation background not change when a page is changed. If I refresh the entire window, the correct nav is active, however the class name does not seem to be updating.

image

image

````

  • Home
  • Search
  • Favourites
  • History
  • Admin

````

Most helpful comment

I had the same issue with router, but solution proposed by @ryanhomer helped (wrap component with "withRouter ")
How it works for me:
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CategoriesMenu));
CategoriesMenu - contains other components with NavLinks and activeClassName

Thank you, sir! :+1:

All 33 comments

Are you using a component that implements shouldComponentUpdate between your router an d your navlinks?

No just a standard component with links

````import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import './Nav.scss'

class Nav extends Component {

render() {
    return (
        <nav>
            <ul>
                <li>
                    <NavLink to="/" exact activeClassName="active" ><span className="fa fa-home"></span>
                    <span className="title">Home</span>
                    </NavLink>
                </li>
                <li>
                    <NavLink to="/search" activeClassName="active"><span className="fa fa-search"></span>
                        <span className="title">Search</span>
                    </NavLink>
                </li>
                <li>
                    <NavLink to="/fav" activeClassName="active"><span className="fa fa-star"></span>
                    <span className="title">Favourites</span>
                    </NavLink>
                </li>
                <li>
                    <NavLink to="/history" activeClassName="active"><span className="fa fa-history"></span>
                    <span className="title">History</span>
                    </NavLink>
                </li>
                <li>
                    <NavLink to="/admin" activeClassName="active"><span className="fa fa-cogs"></span>
                    <span className="title">Admin</span>
                    </NavLink>
                </li>
            </ul>
        </nav>
    );
}

}

export default Nav;````

I think this might be related - I'm also having an issue when the build went from 0.6 to 0.7. When I click on a NavLink, the URL changes in top bar, but the components dont switch at all. This is without any code changes, it just seems to be an issue when I switch versions. My Nav is similar to the one above.

Can you verify that the <NavLink>s are re-rendering? Even if you aren't calling sCU directly, you might have third-party code that is calling it (like connect or observable).

I put together a simple codepen where this is working.

I am running the parent component in a mobx decorator. It calls the Nav. Not sure if that has an effect

````
import React, {Component} from 'react';
import './Header.scss';

import { Link } from 'react-router-dom';
import { observer } from "mobx-react"

import Nav from './Nav';
import User from './User';
import logo from '../logo.png'

@observer
class Header extends Component {
render() {
return (




url(${logo})}} />


GPbook

Referrals made quick and easy

            </Link>
            <Nav />
            <User />
        </header>
    );
}

}

export default Header;
````

My Root component is also wrapped in an observer

````
@observer
class App extends Component {
render() {
return (


{store.user ?






: }

                </div>      
            </Router>
        </Provider>
    );
}

}
````

Yes. observer calls shouldComponentUpdate. Only observers that are children of the <Router> will need to pass the location prop.

I also had the same issue when I tried upgrading to beta 7. Any routes withing a component which was using the react-redux connect HOC (which implements shouldComponentUpdate) would not update.

Is the long term solution to this going to be passing the location prop or is this just a temporary work-around until this gets solved?

@nzjoel1234 Any sCU call should return true when the location changes. Passing the location as a prop will do this, but it already isn't an issue if other props are changing (e.g. you are passing in product props from the store in your mapStateToProps based on a URL param). There are also plans to remove sCU from react-redux. If you really don't want to use a location prop, you can pass the { pure: false } option to connect.

As mentioned before, is this a temporary change, or long term, as it was quite intuitive that NavLink would automatically update itself regardless pre-beta7? This means each time I create a NavLink now, I will need to make sure its additionally passes a location prop all the way down the components, (depending on how deep it is).

Your <NavLink> doesn't need to be passed the location. It is whatever component is calling sCU that needs the location so that it knows that it should allow a re-render when the current and next location props are different.

I apologies to ask again, but I just want to be a bit more clear.

In my example above, the root App component calls the Header, which calls the Nav. I am unsure where I am getting this location prop from, and am I passing it from App > Header to Nav, and never even using it?

I previously felt comfortable knowing that no matter where I place a Link (now NavLink) as long as the to=url, isActive would work, and automatically update activeClassName. However now I can't get head around what is happening differently, before and now, as to those automatic updates no longer happening and now a manual process. I would much rather re-create a new NavLink component which worked as it previously did.

Pretend for a minute that context does not exist. If that is the case, then the only way to pass routing information to children components would be through props. Each child would receive the current location as well as other props from its parent.

If that were the case, then a component that implements shouldComponentUpdate (returning true if a shallow check of its props shows that at least one of them updated) would re-render whenever the location prop changes. That is how mobx-react's observer implements sCU.

Back to reality, where context does exist as a way to pass state. You navigate, the location object changes, and the above component that implements sCU runs. However, now that location is part of the context, sCU does not detect that the location object is different and it returns false, preventing it and its children from updating. When this is happening we do want to update, the problem is just that there isn't a good way for sCU to detect this (at least when using third-party components).

The solution is thus to pass the location as a prop to the component that implements sCU.

You can read more about this and how to access the location in the dealing with updating blocking guide.

@pshrmn Since most people who will get this issue are using either Redux of Mobx would it be possible for react router to allow some type of middleware which could be used to provide a way of sharing state instead of using context?

So on history.onChange react router could call something like middleware.setNewLocation(location) if no custom middleware was provided then this would update the context otherwise some custom middleware could store the new location in Redux state for example. Then the withRouter HOC could retrieve location etc. from the middleware, and Links etc. could also use withRouter to get their props?

Hope this makes sense?

Interesting... I found this issue with my NavBar component. I'm using redux, but I don't have any connect between the router and the NavLinks. The problem I had, it was because I was extending from PureComponent

class NavBar extends PureComponent {
   //...
}

By changing to Component the active class started working.

class NavBar extends Component {
   // ...
}

That's because the PureComponent blocks the update to the components underneath it: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md

I fixed my own problem awhile ago, but this was indeed the issue with me (I was using redux-form. Passing the Form component the location prop made things work again. Thanks for the great docs!

I had to add a pathname property to the component in which I was using the NavLink and make sure it was getting the current pathname from the location property passed down by the Route component. This causes the component to re-render when the pathname changes (even if you are not directly using the pathname property) and allows the NavLink component to update.

I'm on v4.1.1 of react-router and react-router-dom, and getting the same issue. If I got all the above right, I should look for a way to let NavLink know that it should update and render again.

I have a class class MenuListItem extends Component {...}, and inside multiple elements, including <NavLink strict={true} to={this.props.url} className={${styles.container} ${styles.link}} activeClassName={styles.linkActive}>.

I do not know how to proceed, I tried using context but did not do much for me. Any suggestions ?

@ryanhomer I've tried your solution, added pathname prop (passing this.props.location.pathname) to my MenuListItem component, and it changed nothing. Is there any step you did not detail in your explanation that can make it work for me ?

@PDS42 No, that's all I had to do. However, as an alternative and cleaner solution, I ended up wrapping my components using withRouter. In my case, I have a main component in which the routing takes place and which renders Component1 which in turn renders Component2. I had to wrap both Component1 and Component2 in order for the re-render to work. So, something like this.

import { withRouter } from 'react-router-dom';

class Component1 extends React.Component { render() {} };

Component1 = withRouter(Component1);
export default Component1;

Hope this helps.

Unfortunately I already did that, and it did not help (pretty sure you cannot pass the location prop without withRouter anyway?). If you think of anything else please let me know! Thank you for taking the time.

I had the same issue with router, but solution proposed by @ryanhomer helped (wrap component with "withRouter ")
How it works for me:
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CategoriesMenu));
CategoriesMenu - contains other components with NavLinks and activeClassName

Thank you, sir! :+1:

@PDS42 If you are using withRouter and connect together, it has to be done like how @stas-dolgachov did it, withRouter wrapping connect.

@ryanhomer I just realized I actually did not wrap the parent component, but just the one with the in it! For next references, wrapping only the parent with withRouter did the trick for me. Thank you! :)

I had the same and wrapping my App component into withRouter did help.

Same here. Just wrapped the parent component withRouter and it worked.

I was having this problem and my entire App was already wrapped using withRouter. I'm also using Redux. I noticed that it would style the correct link if I manually refreshed the page on any given route which made me think of mapping router to props to trigger an update:

const mapStateToProps = (state) => {
    return {
        router: state.router,
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)

I'm not actually using router anywhere in my component, but just adding that in made the active style work.

component wrapped in connect becoming Pure, if 4th argument passed connect
does not declare opposite.

export default connect(mapStateToProps, mapDispatchToProps, mergeProps, {
true: false })

Pure components does not provide context to their children, and
react-router hugely depends on context.

2017-10-18 5:36 GMT+08:00 rokit notifications@github.com:

I was having this problem and my entire App was already wrapped using
withRouter. I'm also using Redux. I noticed that it would style the
correct link if I manually refreshed the page on any given route which made
me think of mapping router to props to trigger an update:

const mapStateToProps = (state) => {
return {
router: state.router,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)

I'm not actually using router anywhere in my component, but just adding
that in made the active style work.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/4638#issuecomment-337379306,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACJseS9FCX7jPFZdwLfiXCyfp0YrnuN8ks5stR3YgaJpZM4MTapo
.

As @zaynv mentioned, passing the location as a prop to the component holding the NavLink will make active class work again.
In my case I had to get location using withRouter:

class App extends React.Component{
    render() {
        const { location } = this.props
        return(
            <div id="AppWrap">
                <Navigation location={location}/>
                <Route exact path="/" component={Home}/>                
            </div>)
    }
}

export default withRouter(App)

and then in the Navigation component where it holds the NavLinks simply started setting active classes again.

I setup codesandbox demo and I can't get active links to work: https://codesandbox.io/s/m36091p4yp

Damn, weird. The solutions work in one of my apps, but don't in other. Weird 😞

@stas-dolgachov 's answer worked for me (whew!). I now have much smaller problem, hopefully someone here can jumpstart me. My export is using compose:

export default compose(
  connect(mapStateToProps, mapDispatchToProps),
  withStyles(styles, { name: 'App' }),
  withTheme(),
)(Header);

How can rewrite this to have the "withRouter" in the right place? I am weary, begging a handout :)

@lgerndt pretty sure you just have to do this:

export default compose(
  withRouter,
  connect(mapStateToProps, mapDispatchToProps),
  withStyles(styles, { name: 'App' }),
  withTheme(),
)(Header);

Let me know if that worked!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Radivarig picture Radivarig  Â·  3Comments

ArthurRougier picture ArthurRougier  Â·  3Comments

winkler1 picture winkler1  Â·  3Comments

stnwk picture stnwk  Â·  3Comments

Waquo picture Waquo  Â·  3Comments