Closed hacker0limbo closed 2 years ago
:wave: @hacker0limbo, we use the issue tracker exclusively for bug reports and feature requests. However, this issue appears to be a support request. For usage questions, please use Stack Overflow or Reactiflux where there are a lot more people ready to help you out. Please feel free to clarify your issue if you think it was closed prematurely.
I'll make a quick guide on this, it's going to be a common question that we've already anticipated.
Short answer: When your thunk is successful, change the state to something like "success"
or "redirect"
and then useEffect + navigate:
export function AuthForm() {
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
if (auth.status === "success") {
navigate("/dashboard", { replace: true });
}
}, [auth.status, navigate]);
return (
<div>
<button
disabled={auth.status === "loading"}
onClick={() => dispatch(login())}
>
{auth.status === "idle"
? "Sign in"
: auth.status === "loading"
? "Signing in..."
: null}
</button>
</div>
);
}
@ryanflorence it still doesn't answer the original question though, which is how to programmatically use navigation outside React components.
I believe it's not about one particular example that can be fixed with a different workflow, but a more general and pretty demanded functionality that is missing from the new version, if the library is to be called a 'fully-featured' one. Would love to see a guide on how it can be done.
Related: https://github.com/remix-run/react-router/pull/8284, https://github.com/remix-run/react-router/issues/7970.
I had this problem and created custom HistoryRouter
like that:
import { Update } from "history";
import { useLayoutEffect, useReducer } from "react";
import { Router } from "react-router-dom";
// your local created history
import { history } from "./history";
const reducer = (_: Update, action: Update) => action;
export const HistoryRouter: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {
action: history.action,
location: history.location,
});
useLayoutEffect(() => history.listen(dispatch), []);
return (
<Router navigationType={state.action} location={state.location} navigator={history}>
{children}
</Router>
);
};
source code reference: https://github1s.com/remix-run/react-router/blob/HEAD/packages/react-router-dom/index.tsx#L133
temporary plan, waiting for official support:
// history.ts
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
// BrowserRouter.tsx
import React from "react";
import { History } from "history";
import { BrowserRouterProps as NativeBrowserRouterProps, Router } from "react-router-dom";
export interface BrowserRouterProps extends Omit<NativeBrowserRouterProps, "window"> {
history: History;
}
export const BrowserRouter: React.FC<BrowserRouterProps> = React.memo(props => {
const { history, ...restProps } = props;
const [state, setState] = React.useState({
action: history.action,
location: history.location,
});
React.useLayoutEffect(() => history.listen(setState), [history]);
return <Router {...restProps} location={state.location} navigationType={state.action} navigator={history} />;
});
@amazecc and @huczk thanks for your comments :)
It works 👍
Than all you need is to wrap the <App />
with <HistoryRouter/>
.
import { HistoryRouter } from "./HistoryRouter"
import { myHistory } from "./history"
ReactDOM.render(
<HistoryRouter history={myHistory}>
<App />
</HistoryRouter>
document.getElementById("root")
)
and use your myHistory
where you want:
import axios from "axios"
import { myHistory } from "./history"
// Configure axios instance
const backendApi = axios.create(...)
backendApi.interceptors.response.use(function(response) {
return response
}, async function (error) {
if (error.response?.status === 403) {
myHistory.replace(`/forbidden`) // Usage example.
return Promise.reject(error)
}
return Promise.reject(error)
})
@amazecc and @huczk thanks for your comments :)
It works +1
Great, thanks from me too. I am actually using myHistory.replace/.push INSIDE of react components, as it does not trigger a re-render of all components (like useNavigate does), when the route changes. Hope this gets official support....
@ryanflorence do you think this is something react router v6 will support natively? We are also running into this issue. Crucial use case for us: if an API returns 401, token has expired, we want to redirect user to login screen with a message. We previously were able to do this pretty easily. This has become challenging now that the history object is no longer being exposed (and we can't use hooks in the API request code block)
I also encountered this problem, the official seems to be reluctant to support the use of navigation outside the context of react
What is the official answer for dealing with this issue?
You can now use HistoryRouter (as of version 6.1.0) to maintain a global history instance that you can access anywhere:
import { createBrowserHistory } from 'history';
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
let history = createBrowserHistory();
function App() {
return (
<HistoryRouter history={history}>
// The rest of your app
</HistoryRouter>
);
}
history.push("/foo");
@timdorr According to the docs, the history
package is no longer a peer dependency we need to have installed and it's rather a direct dependency of react-router-dom
(link)
So you are suggesting that we import a transitive dependency, which I do not like very much as a solution. (pnpm would also throw an error on this)
What if the react-router-dom
package exported history-related utils itself instead?
@timdorr Hey can you please elaborate more on unstable_HistoryRouter, is this the official solution from react router? why is it called unstable_HistoryRouter, why unstable?, we need to know if we need to downgrade to v5 or to stay at 6.
Today I had to migrate react-router 5 to 6 and had the same question.
Before we used <Router history={} />
because we also use the history in a mobx store outside.
Now I'm asking myself if I't the way to go to use unstable_HistoryRouter
for this case or if this is bad practice?! I couldn't find more information what exactly "unstable" means :)
The unstable_
prefix just signifies you could encounter bugs due to mismatched versions of history
from what react-router
requests itself. While the practical implications are probably going to just create type issues (different types between versions of history
), there could also be API or behavior changes that cause more subtle bugs (such as how URLs are parsed, or how paths are generated).
As long as you're keeping an eye on the version of history
requested by react-router
and ensuring that's in sync with the version used by your app, you should be free of issues. We don't yet have protections or warnings against the mismatch, hence we're calling it unstable for now.
Many thanks for adding unstable_HistoryRouter
! I have projects with a lot of tests that depend on checking history to determine that an isolated component has done the right thing, and I'm excited that I should be able to bring them over to React Router 6 now.
I've run into a roadblock, though, wherever I have a component that uses Navigate
. If I try to render such a component inside a HistoryRouter
, with no Routes
/Route
in between, the render hangs.
I've created a minimal test case and opened bug #8591.
You can now use HistoryRouter (as of version 6.1.0) to maintain a global history instance that you can access anywhere:
import { createBrowserHistory } from 'history'; import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'; let history = createBrowserHistory(); function App() { return ( <HistoryRouter history={history}> // The rest of your app </HistoryRouter> ); } history.push("/foo");
React Router v6 introduces a new navigation API that is synonymous with and provides better compatibility with suspense-enabled apps.
as described in the official docs, does this mean that i'd still be using history api and not navigate api, if i use unstable_HistoryRouter so there is now way to do it with navigate api outside react components ?
This is a really common use case, so do we have a solution for this? Ideally, something that could be done in 1 or 2 lines of code like how it should be like, instead of requiring devs to moving backwards to unstable_HistoryRouter
, which is obviously something v6 discourages devs from doing?
At the moment, the best solution for me if I stick to v6 is not to replace the root <Router />
(I don't think using unstable_HistoryRouter is what I look forward to in production) but to manually pass a navigate
hook from each component. This creates a lot of code redundancy and is much more prone to errors. Same thing for the solution presented by @ryanflorence, especially when you have to deal with the extra state/useEffect hook to handle simple programmatic navigation.
Can we reopen this issue?
This is what I came up with for a simple solution... seems to work OK.
export let extNavigate: (
value: string | PromiseLike<string>
) => void | undefined
const useExtNavigate = () => {
const navigate = useNavigate()
useEffect(() => {
;(async () => {
while (true) {
const navigatePath = await new Promise<string>((resolve) => {
extNavigate = resolve
})
navigate(navigatePath)
}
})()
}, [navigate])
}
Requirement: you need to call useExtNavigate
from somewhere inside your React Router context, only then can you use this by calling extNavigate('/path')
This is what I came up with for a simple solution... seems to work OK.
export let extNavigate: ( value: string | PromiseLike<string> ) => void | undefined const useExtNavigate = () => { const navigate = useNavigate() useEffect(() => { ;(async () => { while (true) { const navigatePath = await new Promise<string>((resolve) => { extNavigate = resolve }) navigate(navigatePath) } })() }, [navigate]) }
Use by calling
extNavigate('/path')
I'm getting the following error using your code: TypeError: extNavigate is not a function at Object.login
This is what I came up with for a simple solution... seems to work OK.
export let extNavigate: ( value: string | PromiseLike<string> ) => void | undefined const useExtNavigate = () => { const navigate = useNavigate() useEffect(() => { ;(async () => { while (true) { const navigatePath = await new Promise<string>((resolve) => { extNavigate = resolve }) navigate(navigatePath) } })() }, [navigate]) }
Use by calling
extNavigate('/path')
plz tell me how to make it work...
Navigate is a component, not a function. I don't think you can use it like that. https://reactrouter.com/docs/en/v6/components/navigate
Navigate is a component, not a function. I don't think you can use it like that. https://reactrouter.com/docs/en/v6/components/navigate
Thanks for your reply, I just deleted my comment and double checked my code, and find out the real workaround is the following :
// api.ts
axios.interceptors.response.use(
(config) => {
return config;
},
(error) => {
if (error.response.data.statusCode === 401) {
localStorage.removeItem('userInfo');
}
return Promise.reject(error);
}
);
then use useEffect
in a global functional component, eg HomeView to detect if the app need to redirect to '/login' page ,
export default function HomeView() {
const navigate = useNavigate();
useEffect(() => {
const userInfo = localStorage.getItem('userInfo');
if (!userInfo) {
navigate('/login');
}
}, []);
return <LayoutView />;
}
Let me summarize the key idea: even if you can't do navigation outside React context use react-router v6, but you can change a shared state(in our case, it's localStorage.userInfo) there, then you can do navigation in a global/shared functional component.
Navigate is a component, not a function. I don't think you can use it like that. https://reactrouter.com/docs/en/v6/components/navigate
Thanks for your reply, I just deleted my comment and double checked my code, and find out the real workaround is the following :
// api.ts axios.interceptors.response.use( (config) => { return config; }, (error) => { if (error.response.data.statusCode === 401) { localStorage.removeItem('userInfo'); } return Promise.reject(error); } );
then use
useEffect
in a global functional component, eg HomeView to detect if the app need to redirect to '/login' page ,export default function HomeView() { const navigate = useNavigate(); useEffect(() => { const userInfo = localStorage.getItem('userInfo'); if (!userInfo) { navigate('/login'); } }, []); return <LayoutView />; }
Let me summarize the key idea: even if you can't do navigation outside React context use react-router v6, but you can change a shared state(in our case, it's localStorage.userInfo) there, then you can do navigation in a global/shared functional component.
Hi, are you sure this code working? remove 'useInfo' shouldn't trigger that useEffect
@luwanhuang The above code only works when user switch to new router path.
We have refactored our code, use window.postMessage('NotAuthorized');
in axios.interceptors code, then listen to the message and do navigation.
axios.interceptors.response.use(
(config) => {
return config;
},
(error: ErrorResponse) => {
if (error.response.data.statusCode === 401) {
localStorage.removeItem('userInfo');
window.postMessage('NotAuthorized');
}
return Promise.reject(error);
}
);
export default function HomeView() {
const navigate = useNavigate();
const location = useLocation();
function onNotAuthorized(event: MessageEvent) {
if (event.data === 'NotAuthorized') {
navigate('/login');
}
}
useEffect(() => {
sessionStorage.setItem('lastPage', location.pathname);
window.addEventListener('message', onNotAuthorized, false);
return () => {
window.removeEventListener('message', onNotAuthorized, false);
};
}, [location]);
return <LayoutView />;
}
@timdorr after this much time has passed, I think it's not really good to keep the unstable_
flag for the HistoryRouter
. Why not just re-exporting history
or createBrowserHistory
in react-router-dom
? This way devs can be sure they are using the same history
version as react-router because it is coming directly from there.
Navigate is a component, not a function. I don't think you can use it like that. https://reactrouter.com/docs/en/v6/components/navigate
useNavigate() hook returns a function. One can also use it as a component. Both ways are demonstrated in the upgrade guide: https://reactrouter.com/en/v6.3.0/upgrading/v5 Btw it seems components docu for RRv6 has been taken offline? All links return 404.
What is the last update on this? How to redirect from a redux slice?
I'm curious about this too. How can I respond to, for instance, auth status changing in order to safely cause a redirect? Do I just set up an observer inside an overarching react component?
Update2: The below gets re-triggered each time a navigation occurs, so that's not a suitable solution either. Back to the drawing board.
Update:, I can use this pattern from a carefully chosen component. It does mean the components and logic are together, but I'm all for it if it is more coherent in the v6 design:
const Header = (props) => {
const dispatch = useDispatch()
const onLogout = () => {
dispatch(startLogout())
}
const navigate = useNavigate()
useEffect(() => {
auth.onAuthStateChanged((user) => {
if (user) {
store.dispatch(startSetExpenses()).then(() => {
navigate("/app/dashboard")
})
} else {
navigate("/")
}
})
})
return (
<>Component</>
)
}
Before react router v6.4, I used this way to navigating outside of components.
import {createBrowserHistory} from 'history'
const history = createBrowserHistory({ window });
export const rootNavigate = (to: string) => {
history.push(to);
};
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<HistoryRouter history={history}>
<App />
</HistoryRouter>
</React.StrictMode>
);
But after update, history router no more available.
Is there a way now to navigating outside of components, without rollback to 6.3 version?
have any offical sulution? history module is not available in v 6.4
Before react router v6.4, I used this way to navigating outside of components.
import {createBrowserHistory} from 'history' const history = createBrowserHistory({ window }); export const rootNavigate = (to: string) => { history.push(to); }; createRoot(document.getElementById('root') as HTMLElement).render( <React.StrictMode> <HistoryRouter history={history}> <App /> </HistoryRouter> </React.StrictMode> );
But after update, history router no more available.
Is there a way now to navigating outside of components, without rollback to 6.3 version?
have any offical sulution? history module is not available in v 6.4
I think you can use <Router />
component with navigator
prop, but you will have to install history
package yourself.
As I could not find a satisfying way to make it work, decision was to create a hook which will be connected to a global routing state and act like a proxy.
Maybe not the best way to achieve it, but works well in our project. One of the benefits of such method - it's unobtrusive, you can keep React ecosystem separately from business logic.
type RoutingState = {
currentUrl?: string
desired?: {
/** Ignores other fields if provided */
goBack?: boolean
/** Replace instead of push */
replace?: boolean
url: string
}
}
export const useRoutingForUseCases = () => {
const state = yourRoutingState()
const navigate = useNavigate()
const routerLocation = useLocation()
useEffect(() => {
if (!state) return
if (state.desired?.goBack) {
navigate(-1)
return
}
if (state.desired && state.currentUrl !== state.desired.url) {
navigate(state.desired.url, { replace: state.desired.replace })
}
}, [state?.desired])
useEffect(() => {
setYourRoutingState({
url: `${routerLocation.pathname}${routerLocation.search}`
})
}, [routerLocation.pathname, routerLocation.search])
}
Is React Router team planning to provide an alternative for the missing createBrowserHistory
method in v6.4?
Being able to control navigation outside React components is something you just need in some cases. Hooks are great, but we can't use them everywhere. Do I need to find a new router for my projects??
Before react router v6.4, I used this way to navigating outside of components.
import {createBrowserHistory} from 'history' const history = createBrowserHistory({ window }); export const rootNavigate = (to: string) => { history.push(to); }; createRoot(document.getElementById('root') as HTMLElement).render( <React.StrictMode> <HistoryRouter history={history}> <App /> </HistoryRouter> </React.StrictMode> );
But after update, history router no more available.
Is there a way now to navigating outside of components, without rollback to 6.3 version?
@sergioavazquez Controlling navigation from outside the router is still possible, you just need to maintain a separate history
dependency yourself (it's not a big problem). See: https://github.com/remix-run/react-router/issues/8264#issuecomment-991271554
@sergioavazquez Controlling navigation from outside the router is still possible, you just need to maintain a separate
history
dependency yourself (it's not a big problem). See: #8264 (comment)
Ok, but how to link history from this dependency to Router, without “unstable_HistoryRouter”? Or it’s no more needed? It will work without it?
@andrey779evseev @Arsikod The official solution is the one described in https://github.com/remix-run/react-router/issues/8264#issuecomment-991271554 using unstable_HistoryRouter
, and my understanding is that unstable_HistoryRouter
is NOT going to be removed, it is prefixed with unstable_
only to indicate that there is a possibility of your external history dependency losing compatibility with the history dependency used internally by react-router, so you must ensure that the versions match when you update react-router. You can find out which version of history you need by looking at react router's package.json https://github.com/remix-run/react-router/blob/main/package.json
What's the migration path for people doing this already before 6.4 and now want to use the new data loader? In other words, how can one use unstable_HistoryRouter
with BrowserRouter
?
@sergioavazquez Controlling navigation from outside the router is still possible, you just need to maintain a separate
history
dependency yourself (it's not a big problem). See: #8264 (comment)
Why is it unstable
? Are you going to remove this in a few months? I'm on the fence about using unstable methods on production. Also having to keep versions in check, so there's no incompatibility sounds like another point of failure.
I'm sure Remix team removed it for a reason, why drop a working feature or label it unstable unless you foresee issues in the future? Also there's @9jaGuy 's question about using this with data loader.
This is a very common use case outside happy path documentation, it'd be great to get a fully supported method.
There's the newly introduced "redirect" method to redirect from outside of the component. it requires version 6.4 I think it might help ...
There's the newly introduced "redirect" method to redirect from outside of the component. it requires version 6.4 I think it might help ...
Sadly, it doesn't work this way redirect('somewhere')
just return a Response object, and then loader does all work. But in my case I need to redirect user to login page from axios interceptors on 401 response status code.
Something like this:
instance.interceptors.response.use(res => {
return res
}, err => {
if(err.response.status === 401) {
// do some stuff for logout
navigate('/login')
}
return Promise.reject(err)
})
Meanwhile, in Remixland...
Aaaand It's Gone!
unstable_HistoryRouter
https://reactrouter.com/en/6.4.0-pre.14/routers/history-router
Spent the past several days attempting to get 6.4 to work on a module federation mfe app without unstable_HistoryRouter (or unsafe_navigationcontext or using any imports from history) so I can use the new data loaders.
Trying with createBrowserRouter / createMemoryRouter for dev and prod.
Wondering if it's possible.
Some examples and up to date docs for 6.4.2 would be nice at this point.
You can still use a custom history without even needing
unstable_HistoryRouter
. If you dig into the source, you will see that a single global history instance is bound to the global window object. If you create a history outside of React:import { createBrowserHistory } from 'history'; let history = createBrowserHistory();
before rendering
<BrowserRouter>
, react router will use the history you have previously created yourself.Obviously this is not supported, but it works.
Yeah, this doesn't appear to be supported in the RR roadmap going forward (no imports from 'history' since 6.0-beta) and based on what I've tried to date it hasn't worked with the new data loaders.
In looking through all of the open and closed issues - this is one of the most active issues for RR right now that needs a solution inline with the roadmap.
Is there any official method to fix this issue? My case is redirecting user to sign in page when getting SESSION_TIMEOUT error code in every single api request, and how can I make it without hacking methods?
Aaaand It's Gone!
unstable_HistoryRouter
https://reactrouter.com/en/6.4.0-pre.14/routers/history-router
I knew it...
Whether it's a timeout or handling errors, we need a way to navigate outside components. I've been using this feature in all large apps I've worked in for at least 5 years now (some my design, some not). Not everything is a React component, What about Redux or Sagas?
I'm happy to remove history as you say in the docs, but we need a way to navigate from outside components. This is not a minor detail I can just remove and adapt.
How to navigate out component when use createBrowserRouter?
In this case, i can't use <HistoryRouter history={history} basename='/'>
help!!!!!!
I am using router5 and was considering migrating my application to v6. The inability to navigate outside a component seems to disregard an enormous group of use cases and people fundamentally. I use redux-saga to manage complex flows. I know over the years, we have had new technologies emerge and other solutions to handle side effects, but with such a large application with 100+ saga files, this just isn't going to happen anytime soon for us.
At least router5 had a way to plug into the redux structure and allow navigations to happen outside of react components. With this being an issue I have had to halt migrating to v6 for now until this is resolved.
I will continue to use router5 which feels more robust (for my use case anyway.) This is a library (router5( that hasnt released a new version nearly 2 years.
What version of React Router are you using?
v6
Steps to Reproduce
In v6 docs, it mentions that we can use
useNavigate()
hook to do navigation, similar to in v5 we directly useuseHistory()
hook. However I am not sure how we can do the navigation outside React context in v6, cause in v5, your can manually provide ahistory
object as a prop toRouter
component, and reference it in other places even it is not inside React context:But how we could achieve the same functionality in v6? I have see a few posts asking similar questions on stackoverflow, but currently there is no solution provided:
Expected Behavior
A common scenario from my experience was consider i have a redux thunk action creator that doing signup logic, and after sending request if success i wish the page can be navigate to home page:
The action is outside React context so i am not able to use
useNavigate()
hook, Although i can do some refactor and move some logic to the React component, but i prefer to keep most business logic inside action since i wish the component are more responsible for UI rendering.Actual Behavior
As mentioned above