Search code examples
reactjsreact-router-v4redux-sagareact-router-redux

How to provide a history instance to a saga?


I would like to redirect to a new page after successful login. The routes (V4) are used like this:

import { browserHistory } from '....browser_history_signleton';
...

class App extends Component {
  render() {
    const { authentication: { isSignedIn } } = this.props;
    return (
      <ConnectedRouter history={browserHistory}>
        <div>
          <Header/>
          <Route exact path="/" component={Home}/>
          <PrivateRoute isAuthorized={isSignedIn} path="/page1" component={PageOne}/>
          <PrivateRoute isAuthorized={isSignedIn} path="/page2" component={PageTwo}/>
        </div>
      </ConnectedRouter>
    );
  }
}

The saga looks like:

import { browserHistory } from '....browser_history_signleton';

export function* loginSaga() {
  while (true) { // eslint-disable-line no-constant-condition
    try {
      const payload = yield take(LOGIN_SUBMIT);
      const raceResult = yield race({
        signin: call(loginRequest, payload),
        logout: take('LOGOUT')
      });
      if (raceResult.signin) {
        const { data }  = raceResult.signin;
        yield put(loginRequestSucceeded(data));
        const redirectUrl = `.....based on location.state.from.pathname`
        browserHistory.push(rediretUrl);
        ...

My main issue is how to share browserHistory. createHistory from history module is not a signleton, so I had to add:

// browser_history_signleton.js
import createHistory from 'history/createBrowserHistory';

export const browserHistory = createHistory();

What is the most efficient way to provide a history instance to a saga?


Solution

  • I've found two options that felt ok and I've used both. I'm curious to see if anyone has issues with either.

    Option 1: Pass the history object around to sagas.

    Its not obvious, but the sagaMiddleware.run function takes a second parameter that's forwarded to the sagas. Ie:

    /wherever/you/start/saga.js
    
    import { createBrowserHistory } from "history";
    import saga1 from "./saga1.js";
    
    const  function* rootSaga({ history }) {
      yield all([saga1({ history })])
    }
    
    const sagaTask = sagaMiddleware.run(rootSaga, { history: createBrowserHistory() });
    

    I learned this here: https://github.com/ReactTraining/react-router/issues/3972#issuecomment-251189856

    This is a clean-ish way of accessing history functionality. In your actual sagas, you'd use the history object like normal.

    ./saga1.js
    
    export default ({ history }) => [
      takeEvery(actions.DO_SOMETHING_THEN_NAVIGATE, function*({ payload }) {
        ...do something
        history.push("/somewhere");
      }),
    ];
    

    Option 2: Have a single saga manage the history object & navigate using actions

    This is an extension of Option 1. Dedicate a saga to "manage" the history object - pushing/replacing using actions. Ie:

    /my/history/saga.js
    
    
    export default ({ history }) => [ // history is passed in ala option 1.
      takeEvery(actions.HISTORY_PUSH, function*({ payload }) {
        const pathname = payload.fooParam;
        yield history.push(pathname);
      }),
      takeEvery(actions.HISTORY_REPLACE, function*({ payload }) {
        yield history.replace({ pathname: payload.barParam });
      }),
    ];
    

    This keeps your redux store and actions clean, free of the weird hacks some of the community proposes - like passing the history object around in actions.

    Let me know what you think.