Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Router's HashRouter redirects to <base> tag url

I have non-SPA server-side application with React application that is limited to current page, /some/static/page. The application has <base href="/"> in <head> on all pages and relies on it, this cannot be changed.

Here is basic example with React 16, React Router 4 and <HashRouter>:

export class App extends React.Component {
    render() {
        return (
            <HashRouter>
                <div>
                    <Route exact path="/" component={Root} />
                </div>
            </HashRouter>
        );
    }
}

All routes can be disabled for testing purposes, but this doesn't change the behaviour.

Here is create-react-app project that shows the problem. The steps to replicate it are:

  • npm i
  • npm start
  • navigate to http://localhost:3000/some/static/page

HashRouter is clearly affected by <base>. It redirects from /some/static/page to /#/ on initialization, while I expect it to be /some/static/page#/ or /some/static/page/#/ (works as intended only in IE 11).

There's a quick splash of Root component before it redirects to /#/.

It redirects to /foo/#/ in case of <base href="/foo">, and it redirects to /some/static/page/#/ when <base> tag is removed.

The problem affects Chrome and Firefox (recent versions) but not Internet Explorer (IE 11).

Why is <HashRouter> affected by <base>? It's used here exactly because it isn't supposed to affect location path, only hash.

How can this be fixed?

like image 325
Estus Flask Avatar asked Mar 24 '18 16:03

Estus Flask


3 Answers

Actually this from history. If you see their code, they use just createHashHistory and set children. So it equivalent with this:

import React from 'react';
import { Route, Router } from 'react-router-dom';
import { createHashHistory } from 'history';

const Root = () => <div>Root route</div>;
export default class App extends React.Component {

  history = createHashHistory({
    basename: "", // The base URL of the app (see below)
    hashType: "slash", // The hash type to use (see below)
    // A function to use to confirm navigation with the user (see below)
    getUserConfirmation: (message, callback) => callback(window.confirm(message)),
  });


  render() {
    return (
      
      <Router history={this.history}>
      <div>Router
        <Route exact path="/" component={Root} />
      </div>
      </Router>
      );
    }
}

It will show same issue you have. Then if you change history code like this:

import {createBrowserHistory } from 'history';

...

history = createBrowserHistory({
    basename: "", // The base URL of the app (see below)
    forceRefresh: false, // Set true to force full page refreshes
    keyLength: 6, // The length of location.key
    // A function to use to confirm navigation with the user (see below)
    getUserConfirmation: (message, callback) => callback(window.confirm(message))
});

then your problem will gone but definitely not use hash. So the problem not from HashRouter but from history.

Because this come from history, let's see this thread. After read that thread, we can take conclusion this is feature from history.

so, if you set <base href="/">, because you are using hash (#), when browser loaded ( actually after componentDidMount) it will append hash (#) in your case some/static/page => some/static/page + / => / + #/ => /#/. You can check in componentDidMount set debugger to catch before append route.


SOLUTION

simply, just remove element <base href> or don't use HashRouter.

If still need but want avoid from specific component, just put this before class:

const base = document.querySelector("base");
base.setAttribute('href', '');

UPDATE

since you want to keep base tag to keep persist link and use hash router, here the close solution I think.

1. Set tag base to empty.

const base = document.querySelector('base');
base.setAttribute('href', '');

put that code in App component (root wrap component) to call once.

2. When componentDidMount set it back

componentDidMount() {
  setTimeout(() => {
    base.setAttribute('href', '/');
  }, 1000);
}

using timeout to wait react done render virtual dom.

This is very close, I think (have test it). Because you are using hash router, link from index html will safe (not override by react but keep by base tag). Also it work with css link <link rel="stylesheet" href="styles.css"> as well.

like image 130
hendrathings Avatar answered Nov 17 '22 13:11

hendrathings


Your observation about HashRouter and the <base> tag is correct. I filed an issue about the differences in browsers here: https://github.com/ReactTraining/history/issues/574 and corresponding PR with fix here: https://github.com/ReactTraining/history/pull/577

In the meantime, I'm not sure about all the routing you need, but if the react app lives entirely under /some/static/page/, you can probably make it work with:

<BrowserRouter basename="/some/static/page">.

like image 30
Elian Ibaj Avatar answered Nov 17 '22 13:11

Elian Ibaj


I ended with HOC that simply applies a fix described in this answer :

function withBaseFix(HashRouter) {
    return class extends React.Component {
        constructor() {
            super();
            this.baseElement = document.querySelector('base');
            if (this.baseElement) {
                this.baseHref = this.baseElement.getAttribute('href');
                this.baseElement.setAttribute('href', '');
            }
        }

        render() {
            return <HashRouter {...this.props}>{this.props.children}</HashRouter>;
        }

        componentDidMount() {
            if (this.baseElement)
                this.baseElement.setAttribute('href', this.baseHref);
        }
    }
};

const FixedHashRouter = withBaseFix(HashRouter);

...
<FixedHashRouter>
    <div>
        <Route exact path="/" component={Root} />
    </div>
</FixedHashRouter>
...
like image 2
Estus Flask Avatar answered Nov 17 '22 13:11

Estus Flask