react-router: does not understand external URL with protocol

If I use <Link to='//google.com'> it’s fine; but if I use <Link to='http://google.com'> I get an error: Uncaught Error: Invariant Violation: Cannot find a route named "http://google.com".

Based on the docs, I believe the <Link> component is supposed to handle full URLs as well as routes, so this appears to be a bug.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 20 (6 by maintainers)

Most upvoted comments

<Link/> is a router-aware anchor, if you want to link to an external site, use an <a/>.

I agree with @davidtheclark . I just ran into this issue and began implementing my own component similar to the one you describe above to handle the logic, but it seemed so basic and obvious a concern and such a common scenario that I stopped to check the react-router docs and issues for the “built-in” solution that I was certain existed. I am surprised that this feature is not built-in.

If that’s the case, then (with docs clarified) this issue still stands but as a feature request rather than a bug. (Also, it’s confusing that the external links work without a protocol?)

Let’s say I have an array of objects representing 20 links, a few of which are “routes” and a few of which are true URLs. I want to map those to components and render them. If <Link> doesn’t understand how to deal with both, then I need to perform the logic myself that distinguishes between external URLs and routes and renders different elements accordingly.

Seems it would be nicer for react-router users if the provided <Link> did this for them. For the situation described above, I’d probably have to end up writing another component wrapping this logic and rendering either a <Link> or an <a> accordingly — don’t other react-router users want the same thing?

Or if other people know of a good existing way (or a better non-existing way) to take care of that use-case, I’d love to hear it. (e.g. page.js hijacks any <a> clicks and checks whether they apply to routes: https://github.com/visionmedia/page.js/blob/master/index.js#L539)

Note, you could make your own Link that does the check and then renders an <a/> or a <Link/>

var YourLink = React.createClass({
  render () {
    return this.isExternal() ? <a/> : <Link/>
  }
});

I guess the docs are wrong. Instead of full URLs it should say full paths. For external links, just use <a href>.

Thanks for the tip, @ryanflorence. I ended up writing this component to handle both:

import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import isExternal from 'is-url-external';

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

/**
 * Link that also works for external URL's
 */
export default class LinkDuo extends Component {
  render() {
    return isExternal(this.props.to) ?
      <a
        href={this.props.to}
        {...this.props}
      />
      :
      <Link {...this.props} />;
  }
}

LinkDuo.propTypes = propTypes;

I think everyone that uses React-Router end up writing a component like that. Good or Bad IDK but annoying indeed 😛

I agree with @shprink and @davidtheclark. If you want to handle dynamic routes (which nearly every app does), you’ll have to add this functionality. Is there any reason that a URL with a protocol and a different domain should NOT be honored? @ryanflorence, would you merge a PR if I added this functionality for you?

Try to use this condition directly

{/^https?:\/\//.test(url)
? <a href={url} />
: <Link to={url} />
}

My solution, including a no to option, an external link and an internal link:

// What <Link> should always be
import React from 'react';
import { Link } from 'react-router-dom';

export default ({ to, children, ...props }) => {

  // It is a simple element with nothing to link to
  if (!to) return <span {...props}>{children}</span>;

  // It is intended to be an external link
  if (/^https?:\/\//.test(to)) return <a href={to} {...props}>{children}</a>;

  // Finally, it is an internal link
  return <Link to={to} {...props}>{children}</Link>;
};

Cannot remove that children (and let react use the props.children from {...props}) because a11y complains about the <a> not having a manual children even if they have from the {...props}.

Since I needed a TypeScript implementation with as few external dependencies as possible I made this drop in replacement of the Link class in react-router-dom (with the help of all your comments). It might be of help to some of you as well:

import * as React from 'react';
import { Link as RouterLink } from 'react-router-dom';

export default class Link extends React.Component<any, {}> {
    constructor(props: any) {
        super(props);
    }

    render(): JSX.Element {
        return /^https?:\/\//.test(this.props.to)
            ? <a href={this.props.to} {...this.props} />
            : <RouterLink {...this.props} />
    }
}

This should probably be covered in the <Link /> component documentation. It is completely unexpected behavior right now.

The docs are misleading through omission – they say that a location object can be provided as the to prop, but do not mention that the protocol and origin properties within that location object are ignored, or that a full url string as the to prop will be applied relatively: href="http://a.example.com/http://b.example.com" instead of href="http://b.example.com"