8

I'm using redux saga & react router v6 and I want to redirect to a route from one of my sagas, is there a way to do it ?

Avedis Maroukian
  • 1,378
  • 3
  • 15
  • 26
  • 1
    Yes, you'll need to create a custom router and history object to do this. Can you update your question to include a [minimal, complete, and reproducible code example](https://stackoverflow.com/help/minimal-reproducible-example) of your code and any attempt to do this on your own first? – Drew Reese Jan 27 '22 at 16:17
  • What do you mean by a custom router ? What I need is from my saga function make a redirect to a route. In a previous version of react you can make `yield put(push(ROUTE))` with connected-react-router library, but it doesn't support v6 of react router @dre – Avedis Maroukian Jan 27 '22 at 16:30
  • Correct. I mean, you implement a custom router using the low-level `Router` in order to provide the custom history object. You can then use this history object as you need elsewhere outside the router/react code. If you need to, create your custom asynchronous navigation actions. My answer [here](https://stackoverflow.com/a/70012117/8690857) may help with the router part, pulling the history object in to issue imperative navigation is left to do. – Drew Reese Jan 27 '22 at 16:48

5 Answers5

11

There are multiple options


1 - Sending the navigate method as part of dispatched redux action

// component
const navigate = useNavigate()
dispatch({type: FOO, navigate})

// saga
yield takeEvery(FOO, function*(action) {
  action.navigate('/foo')
})

Pros:

  • You are using the navigate method which is recommended by the react-router team
  • The API is unlikely to change

Cons

  • You have access to the navigate method only in specific sagas that received such action
  • You have unserializable data in your actions

2 - Another option is to store the navigate method in some way. E.g. you can create a dummy react component that will get the navigate method through useNavigate hook and then store it in some "global" variable. See this SO answer for a possible solution: https://stackoverflow.com/a/70002872/2347716

This deals with the the cons from previous solution, but there are still some issues:

  • You need your React tree to render at least once before you have access to the navigate method
  • You are adding non-view complexity to your view layer by introducing the dummy component

3 - There is another solution, similar to what we had with react-router 5 that deals with the issues in the previous solution. It is to use the history object. It is not documented since its unstable, but there is a HistoryRouter implementation as part of the react-router-dom package. See https://github.com/remix-run/react-router/releases/tag/v6.1.1

import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom'
import { createBrowserHistory } from "history";
const history = createBrowserHistory()

// saga setup
sagaMiddleware.runSaga(rootSaga, history);

// react
<HistoryRouter history={history} />

The issue with this solution is that it is unstable because it might have some issues with some of React 18 features. Personally I prefer it since it solves everything else and we can deal with React 18 issues once its actually released and we know what they are.

Martin Kadlec
  • 4,702
  • 2
  • 20
  • 33
0

My solution

// "HistoryRouter" implementation
import * as React from 'react'
import type {BrowserHistory} from 'history'
import {Router} from 'react-router-dom'

export interface HistoryRouterProps {
    history: BrowserHistory
    basename?: string
    children?: React.ReactNode
}

export function HistoryRouter({
    basename,
    children,
    history,
}: HistoryRouterProps) {
let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
})

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

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


// Use
import {createBrowserHistory} from 'history'
export const history = createBrowserHistory()

ReactDOM.render(
   <HistoryRouter history={history}>
       <Routes>
           <Route path="login" element={<LoginComponent />} />
           <Route path="register" element={<RegisterComponent />} />
           <Route path="*" element={<HomeComponent />} />
      </Routes>
   </HistoryRouter>
, root)

// call history push in file saga
function* fetchLoginSaga(action: FetchLoginRequest) {
    try {
         yield call(history.push, "/home")
    } catch (e: any) {
    
    }
}
Ned
  • 1
  • 1
  • 1
0

I present my solution to the problem, maybe it is a bit convoluted but it is very clean and useful:

All of us will have a parent component in the routes, in my case I have called it App, and we can imagine that we will have this component connected to redux, therefore the following tree would remain:

index.js > Provider > Router > App (with Outlet) > Child Route.

That means that we can create a yield put that changes a props in the parent component, in my case App, which will contain a useEffect that will launch the navigate:

import { useEffect } from 'react'
import { Outlet, useNavigate } from "react-router-dom";

const AppContainer = ({  url }) => {
    const navigate = useNavigate()


    useEffect(()=>{
        if (url) {
            navigate(url)
        }
    }, [url])

    return (
        <div>
            <Outlet />
        </div>
     )
}

export default AppContainer;

Now whenever I want to navigate from the sagas I simply have to execute a yield put, in this way the props url is modified and the useEffect is launched that sends me to the requested url.

Since my SPA system is a bit complex, I'll leave you to solve that part of connecting the dispatch with the component

IvanAllue
  • 434
  • 3
  • 11
0

There is an option that works well for me. You should just take a router (the result of createBrowserRouter() call) and set it to sagaMiddleware context and then use it from saga wherever you want.

App.tsx

const router = createBrowserRouter([
  {
    path: '/',
    element: <div>Home</div>
  },
]);

sagaMiddleware.setContext({
  router,
});

export const App = () => {
  return (
    <RouterProvider router={router}/>
  );
}

saga.ts

export function* selectSmthSaga(action: ActionType<typeof selectSmthAction>) {
  const router: ReturnType<typeof createBrowserRouter> = yield getContext('router');

  router.navigate(`/path/${action.payload}/subpath`);
}
-1

you can use @lagunovsky/redux-react-router instead of connected react router because it's support react router v6

Update: Here's a link to the package which provides further documentation for migrating from conncted-react-router https://www.npmjs.com/package/@lagunovsky/redux-react-router#user-content-migrate-from-connected-react-router

Tjad Clark
  • 552
  • 3
  • 17
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 30 '22 at 19:43