Closed madsmadsen closed 3 years ago
According to the summary on the pull request, basename support was removed in v5:
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.
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!
@bmueller-sykes In the same boat, so following along.
@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?
@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.
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.
FWIW: It looks like React Router 6 will have a mechanism for basenames:
Edit: I get that this isn't a fix for everyone.
@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.
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.
It looks like React Router 6 will have a mechanism for basenames:
I think that history must work without react router or react , It could to be used outside of react mechanism
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.
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
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
@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. (-;
in the same situation, I will be stuck in version 4
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.
I'm also affected by this change in several projects.
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
...
if we use CRA
and we set <base herf="/path" />
inpublic/index.html
, it is be deal?
I think this should work, I don't if it was added later on BrowserRouter
has basename as a prop for the purpose.
@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.
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")
);
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
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/.
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.
@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.
@mukuljainx that's exactly what I've been doing
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.
@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.
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');
@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);
};
@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.
@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.
@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!
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.
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.)
@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
@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.
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.
@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.
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?
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
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
@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!
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?
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?