13

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?

Estus Flask
  • 150,909
  • 47
  • 291
  • 441

5 Answers5

9

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.

Community
  • 1
  • 1
hendrathings
  • 3,572
  • 13
  • 29
  • Thanks for the link to history issue. I'm not sure what [this means](https://github.com/ReactTraining/history/issues/94#issuecomment-211475547) for current problem. Should parsing be added to react-router-dom, because history doesn't support that? *simply, just remove element or don't use HashRouter* - this is not possible. This is non-SPA application where some parts rely on current . I had to temporarily switch to another router that doesn't have this problem. – Estus Flask Mar 28 '18 at 17:09
  • if just avoid to react only, last solution will suitable, I think. – hendrathings Mar 28 '18 at 17:13
  • I keep it in mind, thanks. But I suppose this will break assets that can be loaded after React app initialization. There's not much, but they can be there. Due to how works, modifying it would be very destructive move for the website that relies on it. – Estus Flask Mar 29 '18 at 10:08
  • @estus, I get it now, see my update for more information – hendrathings Mar 29 '18 at 16:34
  • as my update, the `timeout` will help you. Also I think SSR will help the problem since SSR from server like NextJS (my argue). not sure – hendrathings Mar 29 '18 at 16:42
  • Thanks. I'm trying to keep things as straightforward as possible, and random delays have smell. Is it safe to assume that `setTimeout(..., 0)` will work as well? Is setTimeout needed there at all? I guess that in your solution `base.setAttribute('href', '')` goes to `render`? I'm thinking about wrapping HashRouter with HOC for this purpose. – Estus Flask Apr 01 '18 at 13:16
  • I think `setTimeout` will help for `async`, since I don't know how big your react app. You can remove it, just put clean ` base.setAttribute('href', '/');` if your react app is simple. HOC, I didn't think that. How do you accomplish with that? – hendrathings Apr 01 '18 at 13:27
  • As regular HOC, wrapped original HashRouter with another component, just to decouple the fix from . Posted it as an answer, in case this helps somebody else. I don't think that the app matters, because the problem arises inside HashRouter, it occurs only on router init which is sync, to my knowledge. I hope there shouldn't be any async issues (at least this works for me). – Estus Flask Apr 01 '18 at 15:05
  • Nice to know, simply to get rid – hendrathings Apr 01 '18 at 16:12
2

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">.

Elian Ibaj
  • 378
  • 1
  • 8
  • Thanks for your efforts to solve the problem. Consider providing links in the answer for your `history` PRs that are supposed to fix this. I will likely make it accepted answer when PRs are merged. – Estus Flask Apr 04 '18 at 12:57
  • Thanks! The currently accepted answer is an immediate solution to your question so no worries about changing it. – Elian Ibaj Apr 04 '18 at 17:34
2

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>
...
Estus Flask
  • 150,909
  • 47
  • 291
  • 441
1

If you see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base#Hint, it says that using <base> even with #target URLs is expected behavior.

And on https://reacttraining.com/react-router/web/api/HashRouter it says in basename: string section: A properly formatted basename should have a leading slash, but no trailing slash.

So maybe you should define a different basename on the HashRouter element or remove the trailing slash from <base>

Dhruv Murarka
  • 276
  • 6
  • 12
  • Are you suggesting to replace `` with ``? It will defy the purpose, won't it? HashRouter documentation also states that `basename` is base url for hash, not for entire location path, see the example with `` there. If you have different information, please, provide it. – Estus Flask Mar 28 '18 at 12:56
  • Yes `` defies the purpose, but with the current behavior it seems to be a solution. However, `` and `basename` are different, so a better solution might be to keep both `basename = ""` and ``. – Dhruv Murarka Mar 29 '18 at 05:46
  • @estus. Yes, `basename` is base url for hash, not entire location path, but the question is not using it. It uses only `` – Dhruv Murarka Mar 29 '18 at 05:47
  • cannot be a solution because this will break website assets that relies on it (I updated the question to state this explicitly). basename didn't work for me, it affects hash part only, as the documentation suggests. Did you try it? – Estus Flask Mar 29 '18 at 10:03
1

It is an issue of history package. It is even resolved, please look at this pr

As a temporary fix I suggest you to just specify this branch in package.json

"dependencies": {
  ...
  "history": "git://github.com/amuzalevskiy/history.git",
  ...
}

And once fix will be merged into original branch - revert this back to fixed main npm module


Regarding repo: I just did npm run build on the microbouji solution and commited result, as it is impossible to use original repository without running publish script

Andrii Muzalevskyi
  • 2,997
  • 13
  • 18
  • Thanks. This works, although I prefer to avoid using hotfixes from unmerged PRs because in case they are discarded or abandoned, I end up with unmaintained fork. – Estus Flask Apr 03 '18 at 15:42
  • @estus Ok... Personal opinion - keep main code clean and open corresponding PR, usefull for community. And also often "fixing main code to use buggy library" is like a snowball which grows day after day, and at some point it will be easier to stay on older version, than update library, because nobody knows where fixes are :-) – Andrii Muzalevskyi Apr 03 '18 at 17:19