react router 1#rc1 server side redirect - reactjs

I need to redirect a page from another one on server side using the react-router.
The code I wrote it's working on client side, but not in the server render.
you can find the code here:
https://github.com/jurgob/iso-login/blob/e26af0152896a949435db62549027b2683276db7/src/shared/components/LoginPage.js
this is the redirect code inside /src/shared/components/LoginPage.js:
componentWillMount() {
...
this.props.history.replaceState(null, '/home');
}
Note:
If you look on https://github.com/jurgob/iso-login/blob/e26af0152896a949435db62549027b2683276db7/src/shared/routes.js
I did:
function requireAuth(nextState, replaceState) {
// replaceState({ nextPathname: nextState.location.pathname }, '/login');
}
...
<Route path="home" component={HomePage} onEnter={requireAuth} />
this code is working, but I would like to do the redirect inside the component

There's no built-in capacity in React Router to handle redirects within the component on the server side.
This is because onEnter hooks run within the context of match, so React Router can pick up on replaceState calls and notify the match callback of the requested transition. By the time componentWillMount runs, match has already invoked the callback.
You're most likely going to have to build some higher-level abstraction that instruments the history.replaceState call when rendering on the server, then takes the appropriate actions after ReactDOM.renderToString returns.

A couple different ways to handle this:
1 - If you're okay with letting the redirect actually happen on the client side, you can do something like
history.push('/newPath/');
This is the solution I used, in order to not have two different kinds of redirects (on the client and on the server). In my case, I passed in a "context" props (only for the server side portion of the code, so my method looks more like
componentWillMount() {
if (this.props.context) { // context only available on server side
this.props.context.doLogout();
}
this.props.history.push('/newPath/');
}
2 - If you really want the server to do the redirect, then you have to pass down the response object from express or whichever framework you're using:
componentWillMount() {
// you may have to this.props.history.push('/currentPath');
this.props.res.redirect(302, '/newPath/');
}
Happy to elaborate if necessary - I spent some time banging on this and opted for the former solution (code simplicity over correctness, but whatever works for you).

Related

Intercepting and manipulating browser history with react router history

I'm trying to intercept history call in react app and manipulate it to change search parameter of url when it tries to route to certain path. I'm successfully on listening but changing location parameters or replacing history (I want to add ?default_param=value) is not working. It still change location using old parameters. My example code:
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
history.listen(() => {
if (
history.location.pathname === '/path-to-manipulate' &&
!history.location.search
) {
const unblock = history.block();
// this one doesn't change current redirection
history.push({
pathname: '/path-to-manipulate',
search: '?default_param=value',
});
/* this one is also not working
history.replace({
pathname: '/path-to-manipulate',
search: '?default_param=value',
});
*/
unblock();
}
});
Long time ago there was history.listenBefore() https://github.com/ReactTraining/history/issues/379, but it was removed from api and now we have history.block(). I tried with and without block, with different combination of replace/push/forward and event changing history.location explicite. Nothing of that would allow me to properly intercept location before change, alter it and execute with new parameter ?default_param=value.
How to properly intercept location change and change it (location params) before it executes and redirect?
Two more things:
I don't want to mess with <Link to=...>
components and implement logic there. I wanted something global that could manage routing on higher level in react app.
listen() won't work when entering url in browser, it only works when using routing in app. Is there another way to catch first reference on certain location in react app?
What I usually do is try to read the default_param value, and if there is nothing I return with <Redirect to... />
"I wanted something global that could manage routing on higher level in react app."
Why is it? You want a global solution for a problem which is very specific?
If this problem happens a lot (you need a default param), you can write a HOC which takes a parameter and a default value and either return with a <Redirect /> or the wrapped component depends on you have the parameter in the url

In React, OK to always call ReactDOM.hydrate instead of ReactDOM.render?

I've got code like the following where I'm calling ReactDOM.hydrate. This is shared code that sometimes gets called from the node server and sometimes in the client browser. Do I need to do anything different (then calling hydrate) when calling it on the client only. Normally, I'd call render.
const render = Component => {
ReactDOM.hydrate(
<Router history={browserHistory}>
<FullPage />
</Router>,
document.getElementById('root')
)
}
render(App);
hydrate do works similar to render on client side whether the HTML has server rendered markup or not, but when there is no markup previously like not SSR then hydrate produces some warnings but, it will render your markup as expected.
A better way to solve this would be to check if its SSR (assuming root as your parent div id) :
var isMarkupPresent = document.getElementById('root').hasChildNodes();
and then you can either render or hydrate:
isMarkupPresent ? hydrate(...) ? render(...)
Strictly speaking, no it is not safe to always use ReactDOM.hydrate().
From the docs on hydrate, you should only use it on "a container whose HTML contents were rendered by ReactDOMServer". hydrate also expects that the server rendered markup is identical to what the client side render outputs, and any differences should be considered bugs.
ReactDOM.render() on the other hand is used to render your app into an empty container on the client. You may need to do this if you don't have server rendered markup on all pages.
Because render() handles a use case that hydrate() does not, it is not safe to say "you can always use ReactDOM.hydrate()".

Automatic Deep Sub-Route Redirecting in React-Router

I have recently been transitioning a project from AngularJS + UI-Router+ UI-Router-Extras to React + React-Router.
One of the better features in UI-Router-Extras that I'd like to bring to React-Router is called
Deep State Redirect. In this feature, when navigating to a route which has subroutes, the application knows to redirect the user to the last subroute of it that was visited, or if none of its subroutes have yet been visited then it redirects to its first subroute to have been registered.
So for example if the loaded routing tree looks like this:
/main
|_/main_sub_1
|_/main_sub_2
/secondary
and the user starts at route /main/main_sub_2, then goes to /secondary, then goes to /main, they will be automatically redirected to /main/main_sub_2 since /main/main_sub_2 is the last subroute of /main to have been visited.
I know that I could implement this in react router by using
<IndexRedirect to={getLastSubRoute(parentRoute)}> where parentRoute is the full path of the parent <Route> tag, and getLastSubRoute is self-explanitory, but the problem with this is that I would need to add such an <IndexRedirect> tag to every single route I create, which is not optimal since the routes are loaded dynamically, there may be up to 100 subroutes, and much of the application's routing will be written by other people who I shouldn't be relying on to remember to add that tag under every <Route> tag they write.
Ideally, I should be able to apply some function or mixin to the base <Router> tag in the React routing definition to add this functionality to all routing underneath it, but I'm not sure where to start. How might I solve this problem?
Your best bet and possibly the simplest solution would be to set an onChange hook on one of the top level routes. The hook would get called with the next parameter, which would be the next route that the user would be going to.
You would also have the hierarchical structure of routes there (navigating through to parent and children of the parent), so you could dynamically redirect using the replace function, that gets passed in as a parameter also.
I implemented something similar for permission and role management. What I also did was to .bind my store to the function that I pass into the route hook. You could possibly store the route you'd like to redirect to on the user in the state tree. Basically what you refer to as getLastSubRoute.
...
<Route onChange={myRedirectFunctionThatHasStoreBound} .. >
... // other routes
</Route>
...
function myRedirectFunctionThatHasStoreBound(store, prev, next, replace, callback) {
const user = store.getState().user;
const redirectTo = getLastSubRouteForRoute(user, next);
if (redirectTo) {
replace(redirectTo);
}
// don't forget this is you list callback as a param
// your app might stop working, explanation below
callback();
}
If callback is listed as a 4th argument, this hook will run asynchronously, and the transition will block until callback is called.
EDIT: Keep in mind that this will only work if you are using react-router that's newer than or equal to in version to react-router 2.1

Accessing react-router from flummox action/store

I want to be able to make an API call in a Flummox action and transition differently depending on the response. I can pass the router into the action call but am looking for advice on a potentially better way.
UPDATE:
The correct answer is below but I wanted to add some detail to this.
I'm doing an isomorphic app that 1. needs to get data from an api and 2. may need to redirect based on the api response. Whatever I do needs to work through an express.js app and through react.
I made a small lib that does the api call and return some results. I pass it an object (query params object from express for the server-side or a similar object I create for the react-side). This lib makes the request, determines if a redirect is needed and passes back errors, path (string), redirect (boolean), and data (json).
In express, if the redirect boolean is true, I just redirect to it with the current query params. If it's false, I pass the data to flux through an action which updates a store. I then renderToString with react, serialize stores so the clint-side can bootstrap, and send a rendered page to the client.
In react, the redirect boolean isn't important, I get the response back from my lib, pass the data to my flux action, and just transition to whatever the path is. There's really no notion of redirection. Just go to the path no matter what.
Hopefully this is helpful to someone.
In my setup I have my own router module which just wraps the instance of react-router that I create at startup. That makes it easy for any part of the application to just require that module and do what it needs to.
But I would advise you not to have side effects like a call to the router inside your actions. Actions should concern themselves on mutating your application state, and nothing more. It should be possible to call the same action from anywhere in your application which needs to perform the mutation that the action encapsulates.
So if you're switching routes during an action, you're basically tying that action to that particular use case. Let's take an example. You have a todo list, with an input box at the bottom to add a new todo. For that use case, it might be useful to switch route after you saved the todo. Perhaps you switch to Recent Todos or something. But then a new use case comes along where you want to be able to add new todos during another workflow, perhaps the user is planning her week and should be able to add todos on different days. You want the same action that adds a todo, but you certainly don't want to switch routes because the user is still planning the week.
I haven't used Flummox a lot, but from my understanding your Flux object returns whatever the action returns when you trigger an action. So instead of putting the route change inside your action, make sure to return the response from the action and let your component decide if the route should be changed. Something like this:
// todo-action.js
class TodoActions extends Actions {
createMessage(todo) {
return TodoStore.saveToServer(todo);
}
}
// todo-list.js
const TodoList extends React.Component {
render() {
...
}
addTodo(todo) {
this.props.flux.addTodo(todo).then(response => {
if (response.some.prop === someValue) {
this.props.router.transitionTo(...);
}
});
}
}
That way, the action is still nicely decoupled from the route change. If you want to do the route switch in more than one place, you could encapsulate that in a addTodoAndSwitchRoute method in your Flux class.

Backbone Router confusion on howto trigger

I can't seem to get the Backbone Router working in an expected manner. I i) instantiate my Router, then ii) call Backbone.history.start( { pushState: true, root: '/' } ). With the code below...
1) going to "/dashboard" or "/grid", the defined functions are not called
2) when I invoke myrouter.navigate("grid"), the defined functions are not called
**) However, if I then go back or forwards throught the history, then the defined functions are called.
Router : Backbone.Router.extend
routes:
"dashboard": "dashboard"
"grid/:storyid": "grid"
dashboard: ->
console.log("...")
grid: (storyid) ->
console.log("...")
What do I need to do to get cases 1) and 2) to work?
Thanks
Your router is working exactly as it's supposed to. I think your expectations of how it works, and why, are off.
1) going to "/dashboard" or "/grid", the defined functions are not called
When you type "/dashboard" or "/grid" in to your browser's URL bar, your browser makes a request to your server to get that url. This bypasses the router because the browser is making the request back to the server.
The only time typing a URL in to the browser's URL input would not request a new page from the server, is when you are only modifying the hash fragment: "#whatever".
when I invoke myrouter.navigate("grid"), the defined functions are not called
The default behavior of router.navigate is to update the URL w/ the appropriate route, but not that route to be handled by the router - exactly what you are describing.
If you want to force the browser to process the route change, pass true as a second argument: myrouter.navigate("grid", true)
**) However, if I then go back or forwards throught the history, then the defined functions are called.
This works because the router has registered itself w/ the browsers history API and is given the opportunity to handle the URL changes that are caused by the fwd / back buttons, before the browser goes back to the server to get the requested URL.

Resources