remix-run / history

Manage session history with JavaScript
MIT License
8.27k stars 962 forks source link

Where did `basename` go? [v5] #810

Closed madsmadsen closed 3 years ago

madsmadsen commented 4 years ago

I used to create a new history with a basename, this does not seem to be an option anymore.

So how do I set a basename in History v5?

StringEpsilon commented 4 years ago

According to the summary on the pull request, basename support was removed in v5:

https://github.com/ReactTraining/history/pull/751

madsmadsen commented 4 years ago

According to the summary on the pull request, basename support was removed in v5:

751

😢

Should probably be mentioned in the "Breaking changes" section of the release.

bmueller-sykes commented 4 years ago

Maybe I've been "doing it wrong" this whole time, but my React app sits on a subpath of a larger app, and so I used basename to define that (e.g. https://mysite.com/ui/ where ui is the root of the React app.

I define a simple custom history object: const customHistory = createBrowserHistory({basename: '/ui'})

...which I then pass into:

<Router history={customHistory}>

I do this because I want access to history.push outside of the context of React Router (and in fact, I use that history.push method all the time).

So without the basename argument, I don't see how to make my configuration work with history v5.

Is there a "better" way of handling what I need to do? I understand there is a useHistory hook exposed from react-router-dom now, but I'm currently using my history.push method in places outside of a React component (e.g. in various Redux state files, etc).

So am I just stuck on history 4.x forever? Is there something I'm missing?

Thanks!

pgib commented 4 years ago

@bmueller-sykes In the same boat, so following along.

mh-alahdadian commented 4 years ago

@bmueller-sykes exactly like you, I think there should be ways to let us using history out of react context and there should be basename option to let us make our app on a subpath

thank you for opening this issue

now is there any option? what should we do?

bmueller-sykes commented 4 years ago

@MHA15 I don't know what our options are. Was kind of hoping @mjackson or some other maintainer would weigh in. It doesn't seem like our use-case is all that bizarre.

Jony-Y commented 4 years ago

We also have infrastructure over createBrowserHistory and I find the removal of the API without any documentation or a way around it is really odd.

Can some maintainer please provide a W/A or are we supposed to wrap and add the basename to every route? This is critical for us working with a reverse proxy and is also very convenient.

StringEpsilon commented 4 years ago

FWIW: It looks like React Router 6 will have a mechanism for basenames:

https://github.com/ReactTraining/react-router/blob/dev/docs/advanced-guides/migrating-5-to-6.md#move-basename-from-router-to-routes

Edit: I get that this isn't a fix for everyone.

bmueller-sykes commented 4 years ago

@StringEpsilon Hmm...I'm not sure that resolves the issue, though, right? If you want to do a history.push action outside of a route, then that history object, (under RRv6 and history v5) is unaware of the base name, and therefore would potentially execute the wrong route change.

Jony-Y commented 4 years ago

I agree with @bmueller-sykes, for now we will not update our history, but I'm hoping we wont get stuck on v4. Im also wondering why the removal of that API. I doubt it actually hurt the implementation and I see more than a few of us were using it and relying on it in our infra. For my group, this will be a big breaking change.

mh-alahdadian commented 4 years ago

It looks like React Router 6 will have a mechanism for basenames:

https://github.com/ReactTraining/react-router/blob/dev/docs/advanced-guides/migrating-5-to-6.md#move-basename-from-router-to-routes

I think that history must work without react router or react , It could to be used outside of react mechanism

drewloomer commented 4 years ago

I'd be happy to PR adding basename back, but I'd love to hear from a maintainer as to why it was removed before I go through that effort.

cinnabarcaracal commented 3 years ago

The (undocumented) removal of basename has caused us a bunch of trouble. I can't find any reason given, transition guide, depreciation warnings etc. It also looks like after around a month, there has been no response to anyone looking for more information?

Looking at the new basename prop in react-router, it looks like we're going to have to find every place in our code that uses the history ref to navigate programmatically and make them aware of the basename so they go to the correct location? (Maybe we'll write a wrapper for history to intercept .push() and auto-prepend the basename?)

I should add: Thank-you very much for the free software and the support that goes into it, it's just that the way this change has been handled has been a bit frustrating for some of us

cinnabarcaracal commented 3 years ago

Maybe I'm doing something wrong, but in react-router v5 I can't work out how to pass in my own history instance and a basename at the same time.

Looks like basename is a prop on BrowserRouter (which you cannot pass a history instance to) but it's not available on Router (which you can pass a history instance to).

In v5 the best I can come up with to use history from a saga/something that isn't inside a component, while also having a basename, is to use a BrowserRouter and to create a functional component that uses the useHistory hook to get access to history and save it as something like window._history to make it globally available.

import { useEffect } from 'react';
import { useHistory } from 'react-router-dom';

// include this somewhere near the top of your component structure, but below <BrowserRouter/>
export default function HistoryRefSaver() {
  let history = useHistory();

  useEffect(() => {
    window._history = history;
  });

  return null;
}

Then replace my current src/history.js with something like:

function attemptHistoryAction(callback) {
  if (!window._history) {
    console.error('history ref missing');
    return;
  }

  if (!callback) {
    console.error('callback missing');
    return;
  }

  callback(window._history);
}

export default {
  push: route => attemptHistoryAction(history => history.push(route)),
  replace: route => attemptHistoryAction(history => history.replace(route)),
  goBack: () => attemptHistoryAction(history => history.goBack()),
};

Has anyone else worked out a better way to do this?

NOTE: initially based on other's comments I thought this history ref would not be basename aware, but upon testing it seems that it is

bmueller-sykes commented 3 years ago

@cinnabarcaracal that's a clever way of solving this issue, but it sure doesn't seem like it's the way this is "supposed" to be used. And, yeah, I should have mentioned more clearly that I use my independent history object all the time in my redux-saga files, which I very deliberately keep out of React components. So it still begs the question of how RRv6 and historyv5 allow for route changes that sit outside a React component.

...and +1 about the whole free software thing. (-;

jucian0 commented 3 years ago

in the same situation, I will be stuck in version 4

StephanBijzitter commented 3 years ago

This also breaks Sentry integration: https://docs.sentry.io/platforms/javascript/guides/react/integrations/react-router/#react-router-v4v5

The above workaround would work, but ideally you'd want to set Sentry up before rendering anything.

martin-strobel commented 3 years ago

I'm also affected by this change in several projects.

gudorian commented 3 years ago

Probably doesn't cover all use cases, but I did a quick and dirty workaround for a small project I'm working on, feel free to modify if necessary.

Maybe it will be of use to some, at least for an idea how to work around the issue for now.

Create file utils/history.js

import {createBrowserHistory} from "history";

function appendBaseName (basename, to, state, callback)  {
    if (typeof to === 'string') {
        to = basename + to;
    }
    if (typeof to === 'object' && to.pathname) {
        to.pathname = basename + to.pathname;
    }
    if (state !== undefined && state.pathname) {
        to.pathname = basename + state.pathname;
    }

    return callback(to, state);
}

export default function createBrowserHistoryWithBasename(basename = '/') {
    let history = createBrowserHistory();
    history.basename = basename;

    const push = history.push;
    const replace = history.replace;
    history.push = (to, state = undefined) => appendBaseName(basename, to, state, push);
    history.replace = (to, state = undefined) => appendBaseName(basename, to, state, replace);

    return history;
};

Put the following where you import history API

import createBrowserHistoryWithBasename from "./utils/history";

let history = createBrowserHistoryWithBasename('/your/base/path'); // Replace argument with your basename

...
wildfrontend commented 3 years ago

if we use CRA and we set <base herf="/path" /> inpublic/index.html , it is be deal?

mukuljainx commented 3 years ago

I think this should work, I don't if it was added later on BrowserRouter has basename as a prop for the purpose.

bmueller-sykes commented 3 years ago

@mukuljainx The issue is that a valid use-case is to use the history object outside the context of a React component, even within a React application, such as within a Redux state file, or just a plain Javascript file. Not being able to set basename on the history object itself means you cannot do history-based routing outside the context of a React component.

ru4ert commented 3 years ago

My Solution public/manifest.json:

 {
    "start_url": "/subfolder/build/",
 }

package.json:

{
  "homepage": "/subfolder/build/",
 }

Adding <base href="%PUBLIC_URL%/"> to the public/index.html

in my app.tsx/app.jsx(for all JS users):

import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter basename="/subfolder/build/">
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

Advantages

My Version

Just additional info

The compiling looks like this:

PS C:\Users\ruper\Documents\Code\ReactTypescript\myapp> npm run build

> my-app@0.1.0 build C:\Users\ruper\Documents\Code\ReactTypescript\myapp
> react-scripts build

Creating an optimized production build...
Compiled with warnings.

src\assets\images\index.tsx
  Line 22:10:  Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props  jsx-a11y/alt-text

src\component\Home.tsx
  Line 3:3:  'RefObject' is defined but never used  @typescript-eslint/no-unused-vars
  Line 4:3:  'useEffect' is defined but never used  @typescript-eslint/no-unused-vars
  Line 5:3:  'useRef' is defined but never used     @typescript-eslint/no-unused-vars
  Line 6:3:  'createRef' is defined but never used  @typescript-eslint/no-unused-vars

src\component\Profile\Profile_Rupert.tsx
  Line 83:17:  Anchors must have content and the content must be accessible by a screen reader  jsx-a11y/anchor-has-content

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

File sizes after gzip:

  109.68 KB (+147 B)  build\static\js\2.1b5b618e.chunk.js
  29.2 KB             build\static\css\main.048ad125.chunk.css
  5.24 KB (-22 B)     build\static\js\main.e7a21f6b.chunk.js
  1.59 KB             build\static\js\3.c64630cc.chunk.js
  1.18 KB             build\static\js\runtime-main.ba8aaa23.js

The project was built assuming it is hosted at /subfolder/build/.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.

Find out more about deployment here:

  https://cra.link/deployment

PS C:\Users\ruper\Documents\Code\ReactTypescript\myapp>

Here is the new baseurl, extracted from compiling log: The project was built assuming it is hosted at /subfolder/build/.

foobarnes commented 3 years ago

Following up on this. Any more-ideal fixes? We manually set the window state to navigate to page fragments and tabs. We rely on history.location.pathname to include our basename.

mukuljainx commented 3 years ago

@bmueller-sykes what about using history: v4.10.1 with the latest react-router-dom:5 as react-router-dom v5 as still has ^4.9.0 as a dependency so they should work well, I encountered this problem recently, needed a history object which is shared between apps containing their own react-routers. So far everything's working properly even with base prefix paths.

bmueller-sykes commented 3 years ago

@mukuljainx that's exactly what I've been doing

jasperkuperus commented 3 years ago

Unfortunately I also had to revert to history@v4.10.1 to be able to have both a custom history and a basename. Exactly as @bmueller-sykes describes. Can @mjackson maybe pitch in on this topic? The issue is rather old, and I don't see any feedback of any contributor.

bmueller-sykes commented 3 years ago

@jasperkuperus Yeah, it's super bizarre that they haven't chimed in at all. I might not expect them to fix it, but if they came here and said something like "using history like this is an anti-pattern that's going to cause huge headaches for all of us down the road", then that would be something. Their silence is concerning, especially since this is such a crucial (and excellent!) library for so many of us.

FWIW, I've started moving my own codebase away from redux-saga, and more to using async/await/Context (I've found redux and its ecosystem can be overkill, at least for my purposes). As a result, my need for using history outside the context of a React component is much reduced, though I'm still going to have the need for the foreseeable future, so I'm still stuck on 4.x for at least the next 12-18 months I'd say.

mjackson commented 3 years ago

The basename functionality was removed from this package primarily to save on size. History v5 is about 50% smaller than v4, and a lot of that code had to do with supporting the basename feature; both stripping the basename from incoming pathnames when popstate events are received and adding the basename to resolve relative paths in push() and replace().

If you're using React Router, this won't affect you because React Router v6 includes support for basenames both when matching routes and when navigating (i.e. when React Router calls out to the history library, it already knows the full pathname to use for navigation, so it doesn't rely on history's basename feature). More details are in the v5 => v6 migration guide.

If you aren't using React Router, you'll have to construct the full pathnames yourself before sending them to the history library for navigation. This could be as simple as:

let basename = '/my-basename';

function push(url) {
  history.push(basename + url);
}

push('/some/relative/pathname');
bmueller-sykes commented 3 years ago

@mjackson If I'm reading your reply correctly, it means there's no way to solve my issue above until RRv6 comes along (it's not out yet, right? the latest version is 5.2 on Github), is that correct? To recap, I've got a custom history object, which I define like this:

const customHistory = createBrowserHistory({basename: '/path-to-my-app'})

...which I then pass into:

<Router history={customHistory}>

...but I also use completely outside the context of RR (like inside redux-saga functions, etc), with a simple function like this:

export const navigate = (url: string) => {
  customHistory.push(url);
};

So you're saying the new approach is something like (apologies for pseudo-code):

const rootPath = '/path-to-my-app';
const customHistory = createBrowserHistory();
<Router history={customHistory} root={rootPath}>

export const navigate = (url: string) => {
  customHistory.push(rootPath + url);
};
pshrmn commented 3 years ago

@bmueller-sykes, there is a beta release of React Router v6; you can see the source code for that in the dev branch. As for history, you should still be using v4 with React Router v5.

mjackson commented 3 years ago

@bmueller-sykes We are moving away from the idea of using your own custom history object in RR v6. If you need basename support, you can use <Routes basename>. If you need to know when the location changes, you can useLocation(). In general, the idea is to use React Router's API directly instead of calling methods on your own history instance.

bmueller-sykes commented 3 years ago

@pshrmn I am indeed using v4 with RRv5. (-;

@mjackson will we be able to access RR's API from outside React components? e.g. from within a redux-saga state management file? I've started using useNavigate, but of course that can only be used within the context of a React component.

Thanks to you both for replying!

mjackson commented 3 years ago

will we be able to access RR's API from outside React components?

Probably not, no. React Router is a React library, so it's supposed to be used in the context of React.

bmueller-sykes commented 3 years ago

I mean...that's completely reasonable, but then we're left with using two different history objects for app navigation within the react context and outside it, right? Or am I missing something? (I might be missing something.)

cinnabarcaracal commented 3 years ago

@mjackson that is a shame. Redux-Saga is not exactly a niche library within the React ecosystem (and it's not just sagas, it affects anything that is not directly within a component). I guess we will continue to smuggle a reference to the history object into the window object as a workaround

jasperkuperus commented 3 years ago

@mjackson that is a shame. Redux-Saga is not exactly a niche library within the React ecosystem (and it's not just sagas, it affects anything that is not directly within a component). I guess we will continue to smuggle a reference to the history object into the window object as a workaround

Unfortunately I must agree with @cinnabarcaracal here. I do see the train of thought that React Router is made for React. But indeed, redux saga is very common component in people's React stack.

mjackson commented 3 years ago

The goal with history v5 is to make this library just an implementation detail of React Router v6. Nowhere in the RR v6 API do you get a handle on the history object. It's completely hidden.

I understand that may be frustrating to some, but we are carrying around a lot of extra weight in history v4 that just isn't needed in v5 (basenames were a big culprit), and shedding 50% size here makes a huge difference in the overall size of a React Router app. I think the vast majority of RR users will appreciate the size savings in v6.

That being said, the most common use case that I can think of for using history with redux-saga is for navigation. i.e. someone clicks a link/submits a form, you show a spinner while it's validating/submitting, then you navigate to the next page. Even though you can't use the history object directly, this is pretty easy to do with the RR v6 API:

function MyPage() {
  let navigate = useNavigate();

  function handleSubmit(event) {
    // dispatch your Redux stuff here and pass along the navigate()
    // function so you can call it when you're done
    dispatchFormSubmit(event.currentTarget, navigate);
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>...</form>
    </div>
  );
}

One other nice thing about treating this library as an implementation detail is that someday it will hopefully just go away and you won't even need to care. There is a lot of work being done right now on a new window.appHistory API that will someday hopefully make window.history (and this library) obsolete. When that day comes, RR v6 will give you an <AppRouter> component in a minor feature release (not a major breaking release) and you'll be able to upgrade painlessly.

Anyway, like I said I know this is going to require some work but I just wanted y'all to know that we are doing it for a reason and hopefully adapting to this kind of architecture will better prepare you for the future.

bmueller-sykes commented 3 years ago

@mjackson thanks for the thoughtful reply. Truth be told, I hate navigating inside redux saga (if for no other reason than it's too far removed from the components), but for a while there was no other place to really handle complex business logic within a React app--eg if you needed to do a form post, and then conditionally post or get more data based on the reply, redux saga was your best bet. Now that async await is finally mature enough to use, I've been using that as often as I can, but holy cats there's a lot of legacy code in redux saga that may take years to convert over.

But yes, passing down a navigate object might be the most reasonable approach so long as redux saga is around, at least for me.

NicholasGWK commented 3 years ago

This is more of a RR6 question but is related to history, if in RR6 you can't access the history object how do you recommend setting up microfrontends if multiple react apps need access to push / the current route but maybe aren't rendered in the same tree?

abinasp commented 3 years ago

https://github.com/ReactTraining/history/issues/810#issuecomment-824370290

is this working for basename? any solution for this, like how we can add basename in createbrowserhistory

idolize commented 3 years ago

Hey @abinasp you're probably better off using history@4 instead of v5 for now if you're using basename - v5 is more designed for the yet-unreleased react-router@6. See https://github.com/ReactTraining/history/issues/810#issuecomment-825978388

lbassani12 commented 2 years ago

@mjackson is it gonna be possible in RRv6 to dinamically change the basename in <Routes> component?

My use case is a simple language and region separation of my app depending on the IP of the user, then, depending on a selector of languages, I would like to be able to change the basename dinamically and start navigation from that new basename.

Thanks in advance for the answer!

steinarb commented 1 year ago

This broke things for me now when and upgrade to react 18 and router v6 forced me to change from connected-react-router to redux-first-history. Setting the basename in rr v6 doesn't help me.

My app needs to figure out its webcontext path, which may be "/oldalbum" or "/" (if called through a reverse proxy) and it previously set this as the basename of the history.

I used connected-react-router to do programmatic navigation with redux actions from swipe actions.

But connected-react-router doesn't work with react 18 and react router 6 so I had to switch to redux-first-history.

At first the router paths didn't work at all, but then I figured out where react router v6 sets its basename and things started to work when clicking on links.

But when I tried swipe navigation the "/oldalbum" prefix was gone.

So I compared the path in LOCATION_CHANGE redux actions in the pre react v18/rr v6 version of the app and in the post react v18/rr v6 version.

In the pre react v18/rr v6 version both LOCATION_CHANGE events created by react router navigation, and LOCATION_CHANGE sent with push() lacks the "/oldabum" prefix.

In the post react v18/rr v6 version LOCATION_CHANGE events created by router navigation has the "/oldalbum" prefix, and the ones sent with push() lacks "/oldalbum", so they navigate to a non-existing page and 404s.

I thought redux-first-history was to blame, but it looks like this change to history was the cause.

Unsure how to proceed with this? Maybe lock history on v4 forever?