Skip to content
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

[Bug]: How to navigate outside React context in v6? #8264

Closed
hacker0limbo opened this issue Nov 7, 2021 · 56 comments
Closed

[Bug]: How to navigate outside React context in v6? #8264

hacker0limbo opened this issue Nov 7, 2021 · 56 comments

Comments

@hacker0limbo
Copy link

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 use useHistory() hook. However I am not sure how we can do the navigation outside React context in v6, cause in v5, your can manually provide a history object as a prop to Router component, and reference it in other places even it is not inside React context:

// myHistory.js
import { createBrowserHistory } from "history";

const customHistory = createBrowserHistory();

export default  customHistory

// otherFile.js
import history from './myHistory.js'

function doSomething() {
  history.push('/xxx')
}

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:

// signupActions.js

export const signupRequest = (userData) => {
  return dispatch => {
    dispatch(signupPending())

    return axios.post('/api/user', userData)
      .then(res => dispatch(signupSuccess()))
      .then(() => {
        // doing navigation
        // navigate('/')
      })
      .catch(error => dispatch(signupFailure(error.message)))
  }
}

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

@github-actions

This comment has been minimized.

@github-actions github-actions bot closed this as completed Nov 7, 2021
@ryanflorence
Copy link
Member

I'll make a quick guide on this, it's going to be a common question that we've already anticipated.

@ryanflorence ryanflorence changed the title [Bug]: How to naviagte outsite React context in v6? [Bug]: How to navigate outside React context in v6? Nov 7, 2021
@ryanflorence
Copy link
Member

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>
  );
}

@evenfrost
Copy link

evenfrost commented Nov 11, 2021

@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: #8284, #7970.

@huczk
Copy link

huczk commented Nov 15, 2021

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>
  );
};

@amazecc
Copy link

amazecc commented Nov 19, 2021

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} />;
});

@predragnikolic
Copy link

predragnikolic commented Nov 26, 2021

@amazecc and @huczk thanks for your comments :)

It works 👍

history.ts
// make sure to install `history` https://github.com/remix-run/history/blob/main/docs/installation.md
import { createBrowserHistory } from "history"

export const myHistory = createBrowserHistory({ window })
HistoryRouter.ts
// implementation from https://github1s.com/remix-run/react-router/blob/HEAD/packages/react-router-dom/index.tsx#L133-L137
import { ReactNode } from "react"
import { useLayoutEffect, useState } from "react"
import { History } from "history"
import { Router } from "react-router-dom"

export interface BrowserRouterProps {
  basename?: string;
  children?: ReactNode;
  history: History;
}
export function HistoryRouter({
  basename,
  children,
  history
}: BrowserRouterProps) {
  let [state, setState] = useState({
    action: history.action,
    location: history.location
  })

  useLayoutEffect(() => history.listen(setState), [history])

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history} />
  )
}

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)
})

@ayalpani
Copy link

ayalpani commented Nov 26, 2021

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

@nimo-juice
Copy link

@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)

@hsbtr
Copy link
Contributor

hsbtr commented Dec 10, 2021

I also encountered this problem, the official seems to be reluctant to support the use of navigation outside the context of react

@timdorr timdorr mentioned this issue Dec 10, 2021
@c0d3x
Copy link

c0d3x commented Dec 10, 2021

What is the official answer for dealing with this issue?

@timdorr
Copy link
Member

timdorr commented Dec 10, 2021

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 timdorr closed this as completed Dec 10, 2021
@mkarajohn
Copy link

mkarajohn commented Dec 17, 2021

@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?

@Gil-Porsansky
Copy link

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

@ruettenm
Copy link

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 :)

@d-3-n
Copy link

d-3-n commented Aug 27, 2022

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.

@Shaker-Pelcro
Copy link

What is the last update on this? How to redirect from a redux slice?

@freerror
Copy link

freerror commented Sep 14, 2022

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</>
  )
  }

@andrey779evseev
Copy link

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?

@denggj28
Copy link

have any offical sulution?
history module is not available in v 6.4

@Maadtin
Copy link

Maadtin commented Sep 23, 2022

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.

@Oikio
Copy link

Oikio commented Sep 23, 2022

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])
}

@sergioavazquez
Copy link

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?

@evoyy
Copy link

evoyy commented Oct 6, 2022

@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)

@andrey779evseev
Copy link

@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?

@evoyy
Copy link

evoyy commented Oct 9, 2022

@andrey779evseev @Arsikod The official solution is the one described in #8264 (comment) 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

@9jaGuy
Copy link

9jaGuy commented Oct 11, 2022

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
Copy link

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

@Shaker-Pelcro
Copy link

Shaker-Pelcro commented Oct 14, 2022

There's the newly introduced "redirect" method to redirect from outside of the component.
it requires version 6.4
I think it might help ...

https://reactrouter.com/en/main/fetch/redirect

@andrey779evseev
Copy link

There's the newly introduced "redirect" method to redirect from outside of the component. it requires version 6.4 I think it might help ...

https://reactrouter.com/en/main/fetch/redirect

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)
})

@dathmart
Copy link

Meanwhile, in Remixland...
image

@Jamison1
Copy link

Jamison1 commented Oct 16, 2022

Aaaand It's Gone!

unstable_HistoryRouter
Screen Shot 2022-10-16 at 3 29 01 PM

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.

@Jamison1
Copy link

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.

@kyrieLiu3
Copy link

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?

@sergioavazquez
Copy link

Aaaand It's Gone!

unstable_HistoryRouter Screen Shot 2022-10-16 at 3 29 01 PM

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.

@candy4290
Copy link

candy4290 commented Oct 19, 2022

How to navigate out component when use createBrowserRouter?
In this case, i can't use <HistoryRouter history={history} basename='/'>
help!!!!!!

@nigel-loops
Copy link

nigel-loops commented Oct 20, 2022

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.

@Jamison1
Copy link

Found this explanation about the change in accessing history in another issue about RR upgrade from v6.3 to v6.4. Still working through this issue!

Yep! In react-router@6 3 (using history@5 ), there was no router per-se, just a navigator that triggered

@brophdawg11
Copy link
Contributor

Hey folks! I'm centralizing the info around unstable_HistoryRouter over in #9422 (comment) if you want to head over there for an explanation of where things stand!

@fresonn
Copy link

fresonn commented Nov 3, 2022

There is no official solve of this issue still, why?

@ryanflorence
Copy link
Member

ryanflorence commented Nov 3, 2022

React itself is the reason we discouraged navigating outside of the React tree due to UI sharding (a problem with React's concurrent mode and suspense where it would get out of sync with an external store like history).

To unblock our work in React Router, and to make sure we were forward compatible with React, we discouraged this use case and waited for the React team to provide a solution. However, we still provided a path forward for you with unstable_HistoryRouter.

It wasn't until React 18 that useExternalSyncStore was introduced, which is the solution we needed. It's what we've implemented in 6.4 with the new routers that are data aware.

We have a long history of maintaining our old versions. After 8+ years we've only had two breaking releases. Even v3 still gets patches. You can stay on v5 if things in v6 aren't worth the development effort to upgrade. It will be maintained.

If the features in v6 look interesting to you, Matt has outlined some options in the other issue.

@remix-run remix-run locked and limited conversation to collaborators Nov 3, 2022
@ryanflorence
Copy link
Member

Conversation is locked so that further questions happen on the other issue :)

#9422 (comment)

@ryanflorence
Copy link
Member

Also, if you're on unstable_HistoryRouter, this should be a fairly straightforward change to your code to navigate outside of the react tree:

#9422 (comment)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests