Can someone please point me to the correct way of performing navigation outside of components, using V4?
I am looking for solution similar to this
My use case is to perform navigation in redux middleware after my asynchronous operations finish.
I'm not sure if you can access the history from elsewhere, so instead I would manipulate the middleware so that the navigation is done inside of the component that dispatched the action.
The approach that I would take to do this would be to have the middleware return a Promise
that resolves when your async operations finish. Because the middleware is returning a Promise
, the result of your dispatch is thenable. You can add a then
call to the result and do the navigation within that using the context.router
.
Example: https://gist.github.com/pshrmn/a2d0377252ce694d058787cd3b7d61a6
@pshrmn - thanks for your help on this!
The approach you suggested would work, but I am afraid I will slowly end up adding logic into the components: like conditionally navigate based on whether async operations succeeded or whether business rules passed.
Currently I have async operations, business logic and conditional navigation logic in redux saga.
Looking at the redux saga source, I don't think that you would be able to do the Promise thing if you wanted to because you aren't given a chance to manipulate the dispatch's return value in your sagas.
Edit: Removed code I had included because it did not work. Looking more closely at the old browserHistory
code, I see now that each time it was imported, you were getting the same history instance.
I'm not sure if you can access the history from elsewhere, so instead I would manipulate the middleware so that the navigation is done inside of the component that dispatched the action.
This is what I fear will be the solution based on the new direction of v4. I too am in the situation where I need to navigate outside of the components (also using sagas), wonder if @mjackson can chime in on this.
I don't use redux-saga so I don't really have any input here. However, we _do_ put the router
on context. You should probably just grab that and do whatever you want with it.
I guess the task here would be to figure out and document patterns that redux-saga users can use to navigate. Would you agree?
i think the answer is dispatch a navigation action with controlled router
On Sun, Oct 2, 2016 at 12:03 PM Michael Jackson [email protected]
wrote:
I guess the task here would be to figure out and document patterns that
redux-saga users can use to navigate. Would you agree?—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3972#issuecomment-250988673,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGHaE91DtlksEBQvMR6OJjuKw-R1oQTks5qv__2gaJpZM4KJgAC
.
@ryanflorence is the ControlledRouter pushed somewhere?
Closing this as a dup of #3879
I guess the task here would be to figure out and document patterns that redux-saga users can use to navigate. Would you agree?
@mjackson Not really. Sagas patterns aren't really a factor. Just using navigation outside of a component in general. Even if it was inside a method without jsx.
Like the example:
// Somewhere like a Redux middleware or Flux action:
import { browserHistory } from 'react-router'
// Go to /some/path.
browserHistory.push('/some/path')
I was 90% sure I was going to need a component just to listen for navigation to happen, just hoping it was a solution similar to v3.
May be time make a v4 of this doc for v4.
The routers are tied to the type of history
that they create. I think that if someone needs their own instance of a history
object that could be imported throughout their application, they would need to create their own router that doesn't actually create a new history
.
import History from 'react-history/History'
const CustomHistoryRouter = ({history, basename, ...props}) => (
<History createHistory={() => history}>
// copy the StaticRouter function from one of the other Routers
</History>
)
Our approach is to pass the router to the root saga when configuring the Redux store:
const configureStore = (initialState, context = {}) => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
applyMiddleware(sagaMiddleware),
);
sagaMiddleware.run(rootSaga, context);
return store;
};
class ProviderWithRouter extends Component {
constructor(props) {
super(props);
this.store = configureStore(undefined, {
router: props.router,
});
}
render() {
return (
<Provider store={this.store}>
{this.props.children}
</Provider>
);
}
}
const App = () => (
<BrowserRouter>
{({ router }) => (
<ProviderWithRouter router={router}>
<MainLayout>
<Match exactly pattern="/" component={Home} />
<Match pattern="/guide" component={Guide} />
</MainLayout>
</ProviderWithRouter>
)}
</BrowserRouter>
);
The router can then be used directly from the sagas:
export function* goToPage(context, action) {
if (action.page) {
yield call(context.router.transitionTo, action.page);
}
}
export default function* root(context) {
yield [
takeEvery('GO_TO_PAGE', goToPage, context),
];
}
We haven't explored this pattern deeply yet so it may not work long-term but it's working so far.
@trabianmatt i think this is the perfect middle ground, thanks for this!
would importing the history
lib and using it be a solution?
Only thing is, you'd have to create the history within the saga every time:
import createHistory from 'history/createBrowserHistory'
export function* someSaga() {
const history = createHistory()
yield call(history.push, '/some/path');
}
@mmahalwy you wouldn't want to create a new history because it would not share the same subscribers as the one which your router uses, so your router would not be provided route changes made through the new history.
If you do not want to pass the history
to the action, v4 now exports a base <Router>
component which takes a history
prop. This means that you can create a history instance which can be imported throughout your project (similar to v3's browerHistory.js
or hashHistory.js
).
@pshrmn yes but still won't give you the flexibility of importing browserHistory
and using it in the Saga:
import { browserHistory } from 'react-router';
export function* signin(api, { payload }) {
const response = yield call(api.auth.signin, payload);
if (response.ok) {
yield put(AuthActions.signinSuccess({ user: response.data.data }));
yield call(browserHistory.push, '/dashboard');
...
You'd now have to patch to get the browserHistory prop in the sagas, as @trabianmatt shown above.
@mmahalwy What I was describing was creating your own equivalent of browserHistory
that you can import. Previously the exported routers all created a history instance for you and there wasn't a way to import that instance. With the <Router>
component taking a history
prop, you can create a history instance in a module and import it throughout your project so that you don't have to mess around with props to access it.
// history.js
import { createBrowserHistory } from 'history'
// configure, create, and export the project's history instance
export default createBrowserHistory()
// App.js
import history from './history'
const App = () => (
<Router history={history}>
<div>...</div>
</Router>
)
import history from './history'
export function* signin(api, { payload }) {
const response = yield call(api.auth.signin, payload);
if (response.ok) {
yield put(AuthActions.signinSuccess({ user: response.data.data }));
yield call(history.push, '/dashboard');
@pshrmn that is a perfect solution actually. I'll give it a try :)
Thanks @pshrmn! Just a small addendum, with RRv4 beta I had to do the import as
import createHistory from 'history/createBrowserHistory';
@pshrmn nice solution. But for me only the address bar in the browser is updated but the actual route is not displayed. When I manually reload the browser with the new path , the route is rendered correct. Any idea what I'm missing?
@pshrmn thanks for the link. But to my understanding I'm not using any blocking components.
I've created a simple example:
https://codesandbox.io/s/VK38lXpM
After clicking the login button on the /signin
route the path changes to /home
but the Home scene is not shown even though I'm just using <Route>
components which should get the location
prop and thus update...
UPDATE:
The issue was that I was still using the "higher level" BrowserRouter
instead of the low level Router
. Using Router
instead of BrowserRouter
got it working.
Is history
the npm history module or should be importing something from RR4?
eg:
https://www.npmjs.com/package/history
// This is coming from installing history, `yarn add history --save`
import { createBrowserHistory } from 'history'
Also, why the need for a separate file? Why not just do this:
// App.js
import { createBrowserHistory } from 'history'
const App = () => (
<Router history={createBrowserHistory()}>
<div>...</div>
</Router>
)
After reading this it seems like its its a bundled dep?
derp, now I get why there's a separate file. I'll leave this up in case someone else has the same issue in understanding whats going on 📦
The reason for that file export is so that you can reference the same history object which was passed down to your Router, otherwise, history
doesn't refer to anything, and if you called createBrowserHistory(), you'd just keep creating new objects instead of carrying the same one through.
Relevant from @pshrmn
The routers are tied to the type of history that they create. I think that if someone needs their own instance of a history object that could be imported throughout their application, they would need to create their own router that doesn't actually create a new history.
we did something harsh but things are working fine.
we used BrowserRouter as our Router component. as this component put router into its child component context, we grabed this out of its package and put it in our source. in its render method, this component pass history into its component, so I set this component in a variable inside my own history.js.
note: you can find BrowserRouter in here react-router-dom/es/BrowserRouter.js
history.js. we import this history into our sagas.
export let history = {};
export const setHistory = (_history) => {
history = _history;
};
BrowserRouter.js with our modification
import warning from 'warning';
import React from 'react';
import PropTypes from 'prop-types';
import createHistory from 'history/createBrowserHistory';
import Router from 'react-router-dom/es/Router';
import {setHistory} from './history';
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/**
* The public API for a <Router> that uses HTML5 history.
*/
var BrowserRouter = function (_React$Component) {
_inherits(BrowserRouter, _React$Component);
function BrowserRouter() {
var _temp, _this, _ret;
_classCallCheck(this, BrowserRouter);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.history = createHistory(_this.props), _temp), _possibleConstructorReturn(_this, _ret);
}
BrowserRouter.prototype.componentWillMount = function componentWillMount() {
warning(!this.props.history, '<BrowserRouter> ignores the history prop. To use a custom history, ' + 'use `import { Router }` instead of `import { BrowserRouter as Router }`.');
};
BrowserRouter.prototype.render = function render() {
setHistory(this.history);
return React.createElement(Router, { history: this.history, children: this.props.children });
};
return BrowserRouter;
}(React.Component);
BrowserRouter.propTypes = {
basename: PropTypes.string,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number,
children: PropTypes.node
};
export default BrowserRouter;
our some-saga.js
import {history} from "../../history";
import {call} from "redux-saga/effects";
export function* call_login() {
// some logic
yield call(history.push, '/profile');
// some logic
}
It is now possible to do like this :
import { put, takeLatest, all } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { showNotification, USER_LOGOUT, USER_LOGIN_SUCCESS } from 'admin-on-rest';
function* loginSuccess() {
yield put(showNotification('global.success.login'));
yield put(push('/'));
}
function* logoutSuccess() {
yield put(showNotification('global.success.logout'));
yield put(push('/login'));
}
export default function* logoutSaga() {
yield all([
takeLatest(USER_LOGIN_SUCCESS, loginSuccess),
takeLatest(USER_LOGOUT, logoutSuccess),
]);
}
I tried @kopax solution and I can see the action trying to do the push but it doesn't change the url / view.
tried @kopax way, url changes but no render changes
try to wrap Component in withRouter func from react-router-dom
export default withRouter(connect(....)(Component))
2017-10-18 8:07 GMT+08:00 Mike Ni notifications@github.com:
tried @kopax https://github.com/kopax way, url changes but no render
changes—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3972#issuecomment-337416540,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACJsee5y5qdQvDgskd5nFXtXQHMlteufks5stUFBgaJpZM4KJgAC
.
@JustFly1984 worked after switching from BrowserRouter to ConnectedRouter, not sure why
I'm super close to this working as expected but the actions aren't showing up in my Redux dev tools - thus I can't respond to the events with my sagas.
Did what @kopax suggested and here's what I've got in my actions:
function* login({ payload: { email, password } }) {
try {
const profile = yield call(loginUser, email, password);
yield put(setProfileAction(profile));
yield put(setCompletedSetUpAction(true));
yield put(setIsLoggedInAction(true));
yield put(getUserLoggedInAction());
yield put(push(`/dashboard`)) // <---- Here's the push
} catch ({ msg }) {
yield put(getUserLoginError(msg));
}
}
export default function* () {
yield takeLatest(getLoginAction().type, login);
}
And here's my top App.js
which containers the store
and Router
:
class App extends Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<div className="app-container">
<Route path="/login" component={Login} />
<Route path="/dashboard" component={Dashboard} />
</div>
</Router>
</Provider>
);
}
}
The only thing weird is that it doesn't show up in my Redux dev tools as an action... See my screenshots below of everything else working:
Note the last SET_LOGIN_PASSWORD
action below
Notice below that the url did change (some people were saying that wasn't working for them) but there was no corresponding action that shows up in the Redux dev tools even though I did a yield put
. Notice how there is nothing after the last SET_LOGIN_PASSWORD
action.
Note: I did NOT need to do:
export default withRouter(connect(....)(Component))
Just solved this. I wasn't using ConnectedRouter
from the react-router-redux
package.
import {
ConnectedRouter
} from 'react-router-redux';
class App extends Component {
render() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<div className="app-container">
<Route path="/login" component={Login} />
<Route path="/dashboard" component={Dashboard} />
</div>
</ConnectedRouter>
</Provider>
);
}
}
I've found a simple solution.
Dispatch the push function that is inside the component to the saga.
class App extends Component {
foo = () => {
const { dispatch } = this.props;
dispatch({
type: 'ADD_USER_REQUEST',
push: this.props.history.push <<-- HERE
});
}
render() { ... }
}
Inside the saga, just use the push like this:
function* addUser(action) {
try {
yield put(action.push('/users'));
} catch (e) {
// code here...
}
}
Most helpful comment
@mmahalwy What I was describing was creating your own equivalent of
browserHistory
that you can import. Previously the exported routers all created a history instance for you and there wasn't a way to import that instance. With the<Router>
component taking ahistory
prop, you can create a history instance in a module and import it throughout your project so that you don't have to mess around with props to access it.