Feature request
Ideally universal redux users can have consistent methods for redirecting. The Redirect component isn't always appropriate (e.g. inside a saga or componentWillMount phase). So the history method action creators should be an option on the SSR too.
The StaticRouter and react-router-redux middleware are incompatible for server side redirects using react-router-redux actions. The StaticRouter is making it's own custom history instance to modify the context object, but the react-router-redux middleware requires a history instance to be supplied... so the history actions will never reach the StaticRouter's instance.
There 2 options that I can think of:
I think the first option is cleaner, it can be an advanced usage and by default the StaticRouter can create it's own history instance like it currently does.
edit:
I've made a gist of an implementation that I've got working for my first suggestion:
https://gist.github.com/tomatau/9c6011dcb5b9f357368aac2b3a2b1430
Updated the description to be clear I'll make a PR if there aren't any objections to my suggestion!... please someone reply 馃槩
I've come up with an alternative solution, however it is a tad bit hacky and I think @tomatau's method is cleaner. I will post it here FWIW, hopefully illustrates the problem at hand, and leads to more discussion. This seems to me to be an issue that could be encountered by anyone using react-router-redux on the server (one can also simply not use it serverside and use staticContext instead).
The main motivation for myself to implement this was that I wanted a consistent way to handle redirects (with referrer) on client and server. A client redirect might look like this:
@connect(({ auth }) => ({ auth })
class Profile extends Component {
render () {
const { auth, location } = this.props
return auth.user ? (
<h1>Welcome to secret profile page</h1>
) : (
<Redirect to={{
pathname: '/login',
state: { referrer: location }
}} />
)
}
}
Which works fine on client, and the <Login> component will happily read out this.props.location.state.referrer, letting us redirect back to the initial route without dirtying the url. Yay!
But it breaks with <StaticRouter>. <StaticRouter>'s custom history object does not have location.state, as also mentioned here.
But that's OK - we can use the redux store for this sort of state!? Not entirely - in the current implementation, no way for the react-router-redux middleware to access the history object (and therefore keep store.router state up to date).
This was the approach I came up with:
import { routerReducer, ConnectedRouter as _ConnectedRouter, routerMiddleware, replace } from 'react-router-redux'
import createHistory from 'history/createMemoryHistory'
class ConnectedRouter extends _ConnectedRouter {
static childContextTypes = {
router: PropTypes.object.isRequired
}
getChildContext() {
return {
router: {
staticContext: this.props.context
}
}
}
}
const rootReducer = combineReducers({
router: routerReducer
})
app.get('/*', (req, res) => {
const history = createHistory()
const middlewares = [
routerMiddleware(history)
]
const store = createStore(rootReducer, undefined, applyMiddleware(...middlewares))
store.dispatch(replace(req.url))
const routerContext = {}
const App = () =>
<Provider store={store}>
<ConnectedRouter history={history} context={routerContext}>
<Entrypoint />
</ConnectedRouter>
</Provider>
const appHTML = renderToString(<App />)
// Would check routerContext.foo if we were using StaticRouter
// Now, since we have a functional react-router-redux on server, can check that state
const { router } = store.getState()
if (router.location.state && router.location.state.referrer) {
// A <Redirect> (with a referrer ) was rendered
console.log('Going to', router.location.pathname, 'from', router.location.state.referrer.pathname)
const next = router.location.state.referrer.pathname
return res.redirect(302, router.location.pathname + `?next=${next}`)
}
res.send(htmlTemplate(appHTML))
})
Adding staticRouter into context is needed for this.
All that being said, I think a <StaticRouter> w/o react-router-redux on the server is a fine choice (and I'm leaning back towards that), guess it comes down to how important consistency is, and if you need LOCATION_CHANGED middlewares on server.
If something like createStaticHistory existed, that could also be a solution.
Mentioning #4905 as well, since this discussion is relevant to the implementation of a server-rendered react-router-redux app.
I'm also fighting for implementing react-router-redux on server side. I just dropped it and started using just StaticRouter, but it seems not to be the ideal solution.
Hey! Sorry I completely forgot about this issue! I've got the solution I posted working in my boilerplate here:
Server:
https://github.com/tomatau/breko-hub/blob/master/src/server/utils/createStaticHistory.js
https://github.com/tomatau/breko-hub/blob/master/src/server/components/StaticRouter.js
https://github.com/tomatau/breko-hub/blob/master/src/server/middleware/setStore.js
https://github.com/tomatau/breko-hub/blob/master/src/server/middleware/renderApp.js#L13-L26
Client:
https://github.com/tomatau/breko-hub/blob/master/src/app/main.js
This is an important use case for me as I use action creators for location changes. This is an important separation of concerns for me to manage side effects from outside of components... Using <SaticRouter /> on the server render means that all redirects must happen inside components that have access to a context with the router and that simple won't do for me.
edit:
There is one unmentioned caveat with my solution. You should fire a LOCATION_CHANGE directly after creating the store for each request to make the routing reducer in the SSR match the initial render of the client... if that matters to you.
The primary reason for "syncing" the history with the store is to get location updates into state. With a StaticRouter, there are no updates. It's intentionally unchanging and stubs out a history instance to do this.
It instead sounds like you want to use a memory history. This is what v2/3 did inside of match. You can check the state afterwards to determine if a navigation happened. Even cooler, you could write a small middleware to intercept navigation events on the server and handle them in express.
But we shouldn't have to do anything here. You just shouldn't use StaticRouter with RRR.
You just shouldn't use StaticRouter with RRR.
@timdorr your suggestion doesn't make sense.
How can a redirect work on the server that's driven by components without the static context provided by a static router?
I think this should be revisited, been struggling to use react-router-redux with SSR
EDIT:
managed to get it working with redirects on memory history by providing a "fake" staticContext
import { parsePath } from 'history/PathUtils'
// ...express stuff
const initialLocation = parsePath(request.url)
const history = createMemoryHistory({
initialEntries: [initialLocation],
initialIndex: 0,
})
// react-router-redux reducer and middleware inside
const store = configureStore({}, history)
// hack to get <Redirect /> to work on server side without StaticRouter
// @see https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Redirect.js#L47
class FakeStaticContext extends React.Component {
static childContextTypes = {
router: PropTypes.object.isRequired,
}
getChildContext() {
return {
router: {
staticContext: {},
},
}
}
render() {
return this.props.children
}
}
const app = (
<ReduxProvider store={store}>
<FakeStaticContext>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</FakeStaticContext>
</ReduxProvider>
)
const appString = renderToString(app)
if (initialLocation.pathname !== history.location.pathname) {
response.status(302).setHeader('Location', history.location.pathname)
response.end()
return
}
// ...rest of express stuff to return a normal response
@williamoliveira that's essentially the same as my suggestion (which has changed slightly) - although maybe slightly better as the connected router handles the initial location change action dispatch on componentWillMount :)
I'd suggest you change the staticContext from an object literal to the memory history itself, just so that it adheres to the contract that's within existing react-router implementation.
But, to solve this within react router itself, the simplest solution would be to allow passing this history object into the StaticRouter... Right now it falls victim to the OCP and SOC.
@timdorr i think as @williamoliveira suggested above totally makes sense.
We MUST use a ConnectedStaticRouter if we need to give a 302 redirect if there was a <Redirect /> component somewhere out there in the component tree, but with ConnectedRouter we can't.
see here:
and here:
history.push WILL NOT BE CALLED on server side rendering without this router.staticContext
please suggest how to give a proper 302 response in server side with react-router-redux without providing this router.staticContext context?
Most helpful comment
I've come up with an alternative solution, however it is a tad bit hacky and I think @tomatau's method is cleaner. I will post it here FWIW, hopefully illustrates the problem at hand, and leads to more discussion. This seems to me to be an issue that could be encountered by anyone using
react-router-reduxon the server (one can also simply not use it serverside and usestaticContextinstead).The main motivation for myself to implement this was that I wanted a consistent way to handle redirects (with
referrer) on client and server. A client redirect might look like this:Which works fine on client, and the
<Login>component will happily read outthis.props.location.state.referrer, letting us redirect back to the initial route without dirtying the url. Yay!But it breaks with
<StaticRouter>.<StaticRouter>'s customhistoryobject does not havelocation.state, as also mentioned here.But that's OK - we can use the redux store for this sort of state!? Not entirely - in the current implementation, no way for the
react-router-reduxmiddleware to access the history object (and therefore keepstore.routerstate up to date).This was the approach I came up with:
Adding
staticRouterinto context is needed for this.All that being said, I think a
<StaticRouter>w/oreact-router-reduxon the server is a fine choice (and I'm leaning back towards that), guess it comes down to how important consistency is, and if you needLOCATION_CHANGEDmiddlewares on server.If something like
createStaticHistoryexisted, that could also be a solution.Mentioning #4905 as well, since this discussion is relevant to the implementation of a server-rendered
react-router-reduxapp.