-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[v6] Adding useHistory hook #7476
Comments
Also #5210 |
I also need a way to display a Progress bar before the history change then in the final route element calling my progress context to stop the progress when the loading is done. Right now I have a useEffect on my ProgressContext and ViewComponent. The context useEffect is triggerd when it's mounted and also when the location's pathname changed. The useEffect in the view component call stop when the data fetch is done. The useEffect of the view is called before the context then my progress bar is never stopped. Adding a listener on the history could fix my issue by thriggering the context useEffect before the action location change then the view stopping it. Maybe I'm implementing this wrong. |
I found a solution to my probleme by creating a component that return <></> and put this component early in the app tree. Im using useLayoutEffect with my context function to start the progress when pathname change. import React, { useLayoutEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useProgress } from './contexts';
const AppEffects = () => {
const { pathname } = useLocation();
const { startProgress } = useProgress();
/**
* Starts the page progress bar whenever the router path changes.
*/
useLayoutEffect(startProgress, [startProgress, pathname]);
return <></>;
};
export default AppEffects; I need to call context stopProgress in the view compoents when the loading is done. |
I also needed import { createContext, useContext, useRef, useState, useLayoutEffect } from "react"
import { createBrowserHistory } from "history"
import { Router } from "react-router-dom"
const HistoryContext = createContext()
export const useHistory = () => useContext(HistoryContext)
export default function BrowserRouter({ children }) {
const { current: history } = useRef(createBrowserHistory({ window }))
const [{ action, location }, setHistoryState] = useState({
action: history.action,
location: history.location,
})
useLayoutEffect(() => history.listen(setHistoryState), [history])
return (
<Router action={action} location={location} navigator={history}>
<HistoryContext.Provider value={history}>
{children}
</HistoryContext.Provider>
</Router>
)
} My use case is a navigation overlay which needs to close each time user clicks a link. Using @sebascomeau's solution with Alternatively, you could consider exposing just the history change event via a hook, e.g. import { useHistoryListener } from "react-router-dom"
useHistoryListener(state => {
// the same callback you'd pass to `history.listen`
}) |
Some things feel impossible in v6 moving from v5
Before: // Generic component used on every page with a form, it was even possible to display a custom UI for a better-looking prompt by storing location
<Prompt when={ true } message={location => {
if (location.state?.noprompt) {
return;
}
return NavigationBlockMessage;
}}>
const SubmitForm = async () => {
await $.ajax({
...
});
// Simply pass in noprompt to the location.state when we don't need a dialog
history.push('/complete', { noprompt: true });
} v6: const [ SubmitFormWorkaround, SetSubmitFormWorkaround ] = React.useState(true);
usePrompt(NavigationBlockMessage, SubmitFormWorkaround);
const SubmitForm = async () => {
await $.ajax({
...
});
// Now we have to do a complex UseState workaround to not display a popup prompt...
// This will also break if the user clicks other links while loading since the prompt has been disabled in general
SetSubmitFormWorkaround(false);
}
React.useEffect(() => {
if (!SubmitFormWorkaround) {
navigate('/complete');
}
}, [SubmitFormWorkaround]); |
I too am encountering this issue while trying to use React Albus (https://github.com/americanexpress/react-albus). Maybe, a solid option would be to have the app create the browser history instance, then pass it to |
I'm seeing this issue as well on top of an extremely wierd related issue with useLocation hook. Any useEffect with location.pathname or any other property of location as a dependency will get rerun even when the location and location.pathname had never changed(actually location changes without route change) . I'm also working on overlays similar to @malj and I migrated from a perfectly functioning history.location as the useEffect dependency for the same code. v6 unfortunately seems like two steps forward and three backwards because of this and the lack of history access. @sebascomeau seems to be using location.pathname as a dependency. Does this work for you on 6.0.0-beta-0 |
use this simple solution `
` |
I second this. Another problem is that the navigation action (push, pop, replace) is not exposed. This means that if you have code that relies on it then migrating to v6 becomes hard or impossible. E.g. you might have code that always loads fresh data on a forward navigation, but restores the data from an internal cache and restores the scroll position on back navigation. Which one you should use is impossible to know without access to the navigation access. Whether it's the history as such, or functionality that's lost by not exposing it, is another matter. |
I would love to be able to navigate to another route from inside my store (zustand js). window.location.href, is a hard refresh, so definately a no go. I cannot even pass a custom history into react-router's anymore as was possible in v5. Only allowing navigation from hooks is too limited imo. Other than that i really like the v6 - keep up the good work! 💪 |
@asterikx If you really need to run some code before the URL changes, your best bet is to run it in your What I think you mean by "listen to navigation changes before they happen" is you want to run some code before we set state with the new location. We don't currently have a hook for this, but maybe we could. We could probably even give you function App() {
useBeforeLocationChange((prevLocation, nextLocation) => {
// run some sync code...
});
} Would that work for you? |
@mjackson The thing is, by hiding the history - which I totallly understand as a design decision as it's kind of the inner plumbing but not necessarily something you want publicly exposed as a formal API - you've also removed crucial things like the navigation action. There is now no way to know if the user navigated forward or backward (or using "replace") to the current page. And since what you do on a page can depend on it, that is a huge problem. Some examples:
A hook for the navigation action would help in this, but not having any other way to get these things than hooks is also a bit of an issue since hooks don't work nicely with class based components, which both still exist due to legacy reasons but also have their place in other cases e.g. where inheritance is very useful as a design pattern. If you have an existing class-based component and you suddenly can't get access to a million things that you had access to before, since everything is suddenly hooks-based (and some things like the navigation action is not even exposed anymore), that's a really big issue. |
FWIW - I was originally +1 on useHistory, but then I just figured out how to write the code using the available APIs and i think it's actually better than what it was 😂 |
@ssh-peppe We could possibly add another hook for knowing which action triggered the navigation (something like
As far as hooks go, they are a requirement for using v6. There are many well-known strategies for using hooks in class-based components. For example, make a hook-based component that gives you whatever data you need to know in a render prop or higher-order component. import { useLocation } from "react-router-dom";
function GetTheLocation({ children }) {
return children(useLocation());
}
class NoHooksComponent extends React.Component {
render() {
return (
<GetTheLocation>
{location => (
// Render whatever you want with the location here.
)}
</GetTheLocation>
)
}
} |
@rkingon Thanks for sharing 😅 I feel like the approach we are taking in v6 is going to eliminate a lot of bugs in people's apps! |
AFAIK location.key does not work with the hash router, and does not allow you to differentiate between replace and push. And even if those were not issues, kludges using location.key are not exactly elegant solutions to something that has completely legit use cases and had a usable API in previous versions of react router. Generally speaking, it would be great if react router would care more about API stability than it has in the past few years. |
It will in v6. But honestly, you probably shouldn't be building anything with hash router anyway. It's only for legacy apps.
We have had no major breaking API changes since we released version 4 on Sep 13, 2016, almost 5 years ago. The only reason version 5 was a major release was because of a dependency issue, not because of a breaking change. In addition, version 3 has received critical bug fixes all along the way. We care deeply about API stability, and our release history shows it. |
Absolutely, but that choice isn't always one you can make. It's often the case that organizations have huge amounts of existing apps that have been using hash based routing for whatever historical reason, and URLs and users have those bookmarked, the backends can't easily be updated, etc. and you have no choice but to use a hash router. |
... which is why we will still be shipping a It doesn't seem like there's anything really to do on this issue, so I'm going to close. If anyone has a compelling use case for |
My compelling case is for applications that rely heavily on URL's and search param state, like search engines. Listening to the history in this case, and then firing off events based on the search params is very useful, as for example, if a search bar is nested within a header component, but the URL needs to change from within a button component (nested deep within a body component), using the search function from within the header component becomes difficult for the button component. But... If history could be listened to, the button would easily be able to push a new URL, and the parent component could simply render the changes to both the search bar, the results, and the corresponding buttons. Is there a better alternative? |
@joegrenan are you familiar with React’s Context Api? If the search bar gives the query to context provider, then button can easily be a consumer. Personally, that is how I would set it up and not have the search bar update your url- just sent the signal to the provider |
I am getting an odd issue that may or may not be related... Best, |
Not really, because by breaking/deprecating the API with regards to the history action and with regards to programmatic navigation without getting access to a navigation function using a React hook, and a few other things, this is going to be a major headache for several of our apps. |
I have to upvote this issue - I do feel like we are losing important functionality here.
@mjackson In regards to this approach, I understand what you're saying, but the question for me is when are you checking location.key? This is the primary concern for me - a useEffect hook will only detect that the location changes during execution of the render method. Then, and only then will it realize that it needs to run. In my app, it is the navigation action that should cause a reset of some state. Consider my use case - a component A has a routes block inside of it which changes its child component B based on the route. Route change causes component B to change, but component A remains the same component instance. Component B relies upon state passed from component A via props, so component A wants to reset state whenever there is a navigate. Currently, what happens is that, upon navigation, the routing happens, then component A and component B render; at which point component A (by looking at the location during execution of the render method) finds that a 'navigate' has happened, and then subsequently another render due to the state update. It works, but it causes a visual 'flash' that is very nasty. I see navigation as an asynchronous user input, and I don't see why it should be distinguished from any other user action, say, a button click, or something like that. In my mind, component A should be reacting to user input prior to the navigation happening ; but this is not practical behavior to encode into the button click handler.. |
this was great thank you, however in my cause had to delay the update by using a useEffect rather than the ureLayoutEffect you used. as we are using another listener to replace the route back to previous ( with a warning modal ) if detected they are navigating to something unrelated to the work flow of the form the are editing/entering. |
Chiming in on this with a use case I think is reasonable and I do not know how to implement without access to the history object: On our website, on mobile, we have links that cause a MUI drawer panel to slide in from the side of the screen. The panels have a header with a "back" button to slide them closed again, but it's very easy for the user to click the browser back button inadvertently. In this case we don't want to go back to the previous page, we just want to slide the panel closed again. To do this, as far as I can tell, we need to listen on the history object for POP events so we can intercept them when a drawer panel is open. Some screenshots to better illustrate what I mean. The page is a list of sessions; when you click on a session a panel containing detailed information for that session slides in from the left. |
In this case wouldn’t it be possible to store the selected session id in the URL? Either using a Route or a useLocation or useParams hook with conditional rendering. Then it should “just work” when the user presses back, or even refreshes the page. example: /somewebsite/sessions/123 |
Thanks for the suggestion @GeoMarkou - will give this a go and report back. |
yep that worked nicely, thanks again @GeoMarkou ! |
I'm using jotai for state management. With attempts to solve rendering issues. From the the top of their concepts page:
In jotai mounting or unmounting a component, does not always trigger a state change. Instead jotai provides an explicit way to create a hierarchy of atoms by referencing other atoms in code. Like functions referencing other functions. An update to an atom triggers changes to all the atoms according to dependency order, not (un)mount. Except that an atom cannot be updated without a navigation hook. As it stands, the lack of a listener in react-router means that there is no way to trigger an explicit state change triggered by navigation. I may still file this issue under Jotai. They offer a complete API that works and covers all the use cases needed. But could it be that react-router is lacking here. I appreciate how providing less integration options makes it easier to offer new features. But integration is in itself a very important feature. And I think it has been overlooked. |
I think the
history
object should be exposed in some way (also for backward-compatibility reasons).My use case requires to listen to navigation changes before they happen (I have toast messages on my page that I need to dismiss before navigating to another route. Dismissing them after navigating to another route does not work as I might be dismissing toast messages on the page that I'm navigating to as well.)
history.listen
seems to fit my use case (see here), but unfortunately,history
cannot be accessed in v6.The text was updated successfully, but these errors were encountered: