Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I add a class of active to the Link component? #682

Closed
daiky00 opened this issue Jun 1, 2016 · 22 comments
Closed

How can I add a class of active to the Link component? #682

daiky00 opened this issue Jun 1, 2016 · 22 comments

Comments

@daiky00
Copy link

daiky00 commented Jun 1, 2016

I am trying to do this for Example

<a class="link active">Home</a>
<a class="link">About</a>
<a class="link">Contact</a>
<a class="link">Work</a>
<a class="link">Support</a>

this is how my navigation is

    <div className={cx(s.root, className)} role="navigation">
      <Link className={cx(s.link, 'fa fa-dribbble')} to="/dribbble" />
      <Link className={cx(s.link, 'fa fa-behance')} to="/behance" />
      <Link className={cx(s.link, 'fa fa-linkedin')} to="/linkedin" />
      <Link className={cx(s.link, 'fa fa-twitter')} to="/twitter" />
      <Link className={cx(s.link, 'fa fa-instagram')} to="/instagram" />
      <Link className={cx(s.link, 'fa fa-vimeo')} to="/vimeo" />
    </div>

and this the Link Component code.

import React, { Component, PropTypes } from 'react';
import history from '../../core/history';

function isLeftClickEvent(event) {
  return event.button === 0;
}

function isModifiedEvent(event) {
  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}

class Link extends Component {

  constructor(props) {
    super(props);
    this.state = {active: false};
  }

  click() {
    this.setState({active: true});
  }

  static propTypes = {
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
    onClick: PropTypes.func
  };

  handleClick = (event) => {

    let allowTransition = true;

    if (this.props.onClick) {
      this.props.onClick(event)
    }

    if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
      return;
    }

    if (event.defaultPrevented === true) {
      allowTransition = false;
    }

    event.preventDefault();

    if (allowTransition) {
      if (this.props.to) {
        history.push(this.props.to);
      } else {
        history.push({
          pathname: event.currentTarget.pathname,
          search: event.currentTarget.search
        });
      }
    }
  };

  render() {
    const currentPath = this.to.getCurrentPathname();
    const { to, ...props } = this.props; // eslint-disable-line no-use-before-define
    return <a href={history.createHref(to)} id={this.state.path ? 'active' : null} {...props} onClick={this.handleClick} />;
  }

}

export default Link;

Any Ideas?

@daiky00
Copy link
Author

daiky00 commented Jun 1, 2016

Actually @elasim Gave me the answer. The answer is as follows:

import React, { PropTypes } from 'react';
import cx from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Navigation.css';
import Link from '../Link';


function Navigation({ className }) {
  return (
    <div className={cx(s.root, className)} role="navigation">
      <Link className={cx(s.link, 'fa fa-dribbble', { 'active': location.pathname === '/dribbble' })} to="/dribbble" />
      <Link className={cx(s.link, 'fa fa-behance', { 'active': location.pathname === '/behance' })} to="/behance" />
      <Link className={cx(s.link, ' fa fa-linkedin', { 'active': location.pathname === '/linkedin' })} to="/linkedin" />
      <Link className={cx(s.link, ' fa fa-twitter', { 'active': location.pathname === '/twitter' })} to="/twitter" />
      <Link className={cx(s.link, ' fa fa-instagram', { 'active': location.pathname === '/instagram' })} to="/instagram" />
      <Link className={cx(s.link, ' fa fa-vimeo', { 'active': location.pathname === '/vimeo' })} to="/vimeo" />
    </div>
  );
}

Navigation.propTypes = {
  className: PropTypes.string
};

export default withStyles(s)(Navigation);

Note this is throwing me an error in the console

warning.js?85a7:44Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) <div class="App_container
(server) <div data-reactroot="" da

Any Idea on how to solve that?

@koistya
Copy link
Member

koistya commented Jun 2, 2016

With history module v2.0+ I think there should be an API method history.getCurrentLocation().pathname which might be a better option to use than window.location.pathname, so it could work in isomorphic environment.

@elasim
Copy link

elasim commented Jun 2, 2016

I didn't know that api. that's definitely better. use history.getCurrentLocation()

@daiky00
Copy link
Author

daiky00 commented Jun 2, 2016

@koistya @elasim

I put it like this in this Boiler plate

import React, { PropTypes, Components } from 'react';
import cx from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Navigation.css';
import history from '../../core/history';
import Link from '../Link';

function Navigation({ className }) {
  return (
    <div className={cx(s.root, className)} role="navigation">
      <Link className={cx(s.link, 'fa fa-dribbble', { 'active': history.getCurrentLocation().pathname === '/dribbble' })} to="/dribbble" />
      <Link className={cx(s.link, 'fa fa-behance', { 'active': history.getCurrentLocation().pathname === '/behance' })} to="/behance" />
      <Link className={cx(s.link, ' fa fa-linkedin', { 'active': history.getCurrentLocation().pathname === '/linkedin' })} to="/linkedin" />
      <Link className={cx(s.link, ' fa fa-twitter', { 'active': history.getCurrentLocation().pathname === '/twitter' })} to="/twitter" />
      <Link className={cx(s.link, ' fa fa-instagram', { 'active': history.getCurrentLocation().pathname === '/instagram' })} to="/instagram" />
      <Link className={cx(s.link, ' fa fa-vimeo', { 'active': history.getCurrentLocation().pathname === '/vimeo' })} to="/vimeo" />
    </div>
  );
}

Navigation.propTypes = {
  className: PropTypes.string
};

export default withStyles(s)(Navigation);

and is throwing an error

TypeError: _history2.default.getCurrentLocation is not a function
    at Navigation (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:2784:3), <anonymous>:39:165)
    at ReactCompositeComponentMixin._constructComponentWithoutOwner (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5298:3), <anonymous>:294:27)
    at ReactCompositeComponentMixin._constructComponent (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5298:3), <anonymous>:262:21)
    at ReactCompositeComponentMixin.mountComponent (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5298:3), <anonymous>:181:21)
    at Object.ReactReconciler.mountComponent (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:1170:3), <anonymous>:46:35)
    at ReactCompositeComponentMixin.performInitialMount (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5298:3), <anonymous>:357:34)
    at ReactCompositeComponentMixin.mountComponent (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5298:3), <anonymous>:244:21)
    at Object.ReactReconciler.mountComponent (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:1170:3), <anonymous>:46:35)
    at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5454:3), <anonymous>:215:44)
    at ReactDOMComponent.Mixin._createContentMarkup (eval at <anonymous> (http://localhost:3001/assets/main.js?329bbc8bb82cc3dee3ce:5316:3), <anonymous>:611:32)

@frenzzy
Copy link
Member

frenzzy commented Jun 2, 2016

@daiky00 see changelog:

3.0.0-0 (Mar 19, 2016)

  • Added history.getCurrentLocation() method

@daiky00
Copy link
Author

daiky00 commented Jun 2, 2016

@frenzzy so this API only works in history module v 3.0? wow let me try that right now

@daiky00
Copy link
Author

daiky00 commented Jun 2, 2016

@frenzzy, @elasim, @koistya

I install history module v 3.0.0 and is not throwing an error anymore but the active class is not been added to the link. So this API is not working apparently any feedback will be much appreciated.

@arvigeus
Copy link

arvigeus commented Jun 3, 2016

Try logging the path somewhere to check if it's the same with what you're expecting (e.g. '/dribbble/' instead of '/dribbble'). Probably you should use regex to match path, because this won't work for '/dribbble/subpath'.

@daiky00
Copy link
Author

daiky00 commented Jun 3, 2016

@arvigeus I already try that and the path is always '/' root

@awesomejerry
Copy link

With Redux, I'm currently using this hack:

server.js

app.get('*', async (req, res, next) => {
    ...
    store.dispatch(setRuntimeVariable({
      name: 'currentPathname',
      value: req.path,
    }));
    ...
});

client.js

  const removeHistoryListener = history.listen(location => {
      if (currentLocation !== null) {    // to prevent first-time calling
          store.dispatch(setRuntimeVariable({
            name: 'currentPathname',
            value: location.pathname,
          }));
      }

    ....
  });

MyComponent.js

....
export default connect(state => ({
  pathname: state.runtime.currentPathname,
}))(withStyles(s)(MyComponent));

just sharing, hoping to find a more robust and redux-free method.

@elasim
Copy link

elasim commented Jun 6, 2016

@daiky00 In reactjs, render() not work until you change state using setState() or prop on parent component

@daiky00
Copy link
Author

daiky00 commented Jun 6, 2016

@elasim Yeah when I render I make sure I change the state before I console.log the element and I already knew that but thanks for the tip. I just change the whole structure of the project and use react router instead. I don't like the universal router currently in this boiler plate. React Router should be in this Boiler Plate +1 for that

@kaitlynreese
Copy link

I'm having a similar issue where adding a class on the client side causes a React checksum warning since the code doesn't match between client and server. history.getCurrentLocation().pathname always returns / on the server side, and the actual pathname on the client, i.e. /login. Can anyone point me in the right direction on when/how to add the class?

This is the message that I get in the browser:

warning.js?85a7:44Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) resentation" class="active" data-reactid
 (server) resentation" class="" data-reactid="31">

Thanks for your help!

@koistya
Copy link
Member

koistya commented Jul 13, 2016

@rkait good catch! It might be better idea to initialize a new history object for each HTTP request inside server.js/app.get('*', ...) method (SSR). Also pass it as a context variable to React application, and access from inside React components via context, e.g. this.context.history.getCurrentLocation().pathname. A PR with this feature is welcome!

@frenzzy
Copy link
Member

frenzzy commented Jul 13, 2016

@rkait have you tried something like this?

const yourRoute = {
  path: '/some-page',
  action({ path }) {
    return <div>Current Path: {path}</div>
  }
}

or

const routes = {
  path: '/',
  async action({ path, next, render }) {
    const component = await next();
    if (component !== undefined) {
      return render(<Layout currentPath={path}>{component}</Layout>);
    }
  },
  children: [ /* Nested Routes Here */ ]
}

@kaitlynreese
Copy link

I ended up using the solution with redux from @awesomejerry above. Would love to know if there is a better solution in the future. Thanks!

@jlVidal
Copy link

jlVidal commented Aug 7, 2016

I saw some workarounds, but why history.getCurrentLocation() doesn't return the right URL on the server side? What I should I do to get that right? (it would be nice if it's possible avoid using argument/parameter on the render components)

@ghughes27
Copy link

ghughes27 commented Nov 21, 2017

@kaitlynreese @jlVidal I needed to highlight the active navigation tab - might be overkill but I solved the issue by making a component that used state to track whether the current url path was active.

HeaderLink.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import Link from '../Link';
import s from './HeaderLink.css';
import history from '../../history';

class HeaderLink extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isActive: false,
    };
  }
  componentDidMount() {
    this.onPath();
  }
  componentWillReceiveProps() {
    this.onPath();
  }
  onPath() {
    if (history.location.pathname.includes(this.props.to)) {
      this.setState({ isActive: 'active' });
    } else {
      this.setState({ isActive: false });
    }
  }
  render() {
    return (
      <Link
        to={this.props.to}
        className={cx(s.link, this.state.isActive && s.active)}
      >
        <span>{this.props.title}</span>
        {this.state.isActive && <span className={s.activeAfterStyle} />}
      </Link>
    );
  }
}

HeaderLink.propTypes = {
  to: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
};

export default withStyles(s)(HeaderLink);

Navigation.js

import React, { Component } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import HeaderLink from './HeaderLink';
import s from './Navigation.css';

class Navigation extends Component {
  render() {
    return (
      <nav className={s.root}>
        <div className={s.navMenu}>
          <div className={s.navPages}>
            <HeaderLink title="Home" to="/" />
            <HeaderLink title="About" to="/about" />
            <HeaderLink title="Contact" to="/contact" />
          </div>
        </div>
      </nav>
    );
  }
}

export default withStyles(s)(Navigation);

@amypellegrini
Copy link

I got here looking for an answer to this problem, and after reading this thread I think I will just go for the custom link solution provided in the docs, and styling the nested link with a container class.

@ErBahuguna
Copy link

Use NavLink to specify active classname without any extra work.
like this
import { NavLink } from 'react-router-dom'

About

@btargac
Copy link

btargac commented May 17, 2020

I used application context to get the current path of the router, since History is used only on the client side, the below code works on both server and client;

import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import ApplicationContext from 'ApplicationContext';
import history from '../../history';

function isLeftClickEvent(event) {
  return event.button === 0;
}

function isModifiedEvent(event) {
  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}

function handleClick(props, event) {
  if (props.onClick) {
    props.onClick(event);
  }

  if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
    return;
  }

  if (event.defaultPrevented === true) {
    return;
  }

  event.preventDefault();
  history.push(props.to);
}

const Link = React.forwardRef((props, ref) => {
  const { context } = useContext(ApplicationContext);
  const { activeClassName, to, children, ...attrs } = props;

  const isActive = context.pathname === to;
  attrs.className = isActive
    ? `${attrs.className} ${activeClassName}`
    : attrs.className;

  return (
    <a ref={ref} href={to} {...attrs} onClick={e => handleClick(props, e)}>
      {children}
    </a>
  );
});

if (__DEV__) {
  Link.displayName = 'Link';
}

Link.propTypes = {
  activeClassName: PropTypes.string,
  to: PropTypes.string.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func,
};

Link.defaultProps = {
  activeClassName: 'active',
  onClick: null,
};

export default Link;

@ulani
Copy link
Member

ulani commented May 27, 2021

@daiky00 thank you very much for crating this issue! Unfortunately, we have close it due to inactivity. Feel free to re-open it or join our Discord channel for discussion.

NOTE: The main branch has been updated with React Starter Kit v2, using JAM-style architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests