In my project i need to call transitionTo("/some/path")
in a function to go to a different location.
That function is available in the router context, why not pass it down as a prop to the matched component?
My solution was to wrap match with the router
from the context
import React from 'react';
import {Match, propTypes} from 'react-router';
const CustomMatch = (props, context) => {
return (
<Match {...props} render={(matchProps) => {
const newProps = {...matchProps, router: context.router, ...props};
if (props.component)
return React.createElement(props.component, newProps);
else
return props.render(newProps);
}}/>
);
};
CustomMatch.contextTypes = {
router: propTypes.routerContext
};
export default CustomMatch;
I think we want to discourage imperative code as much as possible. It's almost always possible to do things declaratively.
However, there are edge cases and it's a matter of seeing how common they are. I know the plan is to put up resistance to API bloat because we want to be careful not to have to maintain too many APIs or make bad decisions with them. That was problematic in the 0.x and 1.0 days, so we don't want to repeat history (pun intended).
I think we want to discourage imperative code as much as possible. It's almost always possible to do things declaratively.
That's fine, but don't make it _impossible_. Sometimes imperative code is the right tool for the job.
I think we want to discourage imperative code as much as possible. It's almost always possible to do things declaratively.
This is just as saying that there should be no event handlers in React, IMHO.
This is just as saying that there should be no event handlers in React, IMHO.
Once you're rendering, react gives you 4 imperative methods: setState
, forceUpdate
, replaceState
, and isMounted
.
setState
and forceUpdate
are the only ones that we are encouraged to use, and even forceUpdate
should be used rarely, which leaves us with effectively only one imperative method: setState
. So I don't think it's like saying there should be no event handlers. Otherwise there would would be no way to set state!
The whole point of setState
is to change the state of your app and render something else.
The idea behind hiding all of our imperative API behind components is an effort to push all changes to state in your app (including the URL) to setState
. Using router.transitionTo
we now would have two ways to change the state of an app. It's the same reason we use controlled inputs instead of refs and DOM API like value or setAttribute.
By using Just Componentsâ„¢, we have effectively pushed all imperative code up to your app's opinions on state management. Are you using redux? Navigate with dispatch
, component state? navigate with setState.
In other words, by using components and attempting to completely hide our imperative API, every question about routing can be answered by your app instead of our docs. How do you provide values to inputs? How do you change the active tab? How do you add another row to this table? You setState and render a component. Or with redux you dispatch and render. Or with mobx you set observable values and render.
The push-back we're getting is identical to the push-back we see when we talk about controlled inputs at our workshops. It's hard to rewire your brain to think this way, but I think it's worth it.
There's also a chance we've pushed the idea a bit too far here. I'm open to that, so we'll provide a withRouter
HOC that passes the router as a prop to any component you want, but the docs will discourage its use while we continue exploring declarative routing.
I have an example where router.transitionTo
seems to be much better than <Redirect
.
This morning I was working on building an Overlay view for my app. It's just a container that animates in. So that means it actually needs to always be mounted. I can do that by passing a function to Match
:
<Match pattern=":id/edit">
{({matched, ...props}) => (
<Overlay
{...props}
open={matched}
component={EditCollection} />
)}
</Match>
The overlay includes a button that closes itself by navigating to where the user came from. (not a back button, but pushing a new item into history that is the same as the previous one). If I setState
and <Redirect
, then if the user navigates back to the overlay it instantly redirects.
I can solve this by checking this.state
in componentDidUpdate
and reset the redirect flag if it shows up, but there's probably a reason that's a bad idea. I assume this eslint rule exists for some reason I don't now the details of. The intro talks about "property/layout thrashing" by causing extra renders. That probably won't break anything here, but setting a state for a single render and automatically resetting it doesn't seem like a good idea to me.
componentDidUpdate(prevProps, prevState) {
if (this.state.redirect) { // I'm not sure if I need this check
this.setState({ redirect: null })
}
}
So for this situation where I have a component that doesn't unmount when the route changes, using router.transitionTo
was a better fit.
Why not use a ?
blast, i'm on a phone so i can't edit the previous comment.
Why not use a <Link/>
?
Our Overlay is a modal with a specific style/animation. The child typically contains a form of some kind and needs to be able to close itself after a Save, Cancel, or Delete operation. Because Save and Delete should not close until the operation completes, it needs closeOverlay()
to be passed down.
Cancel and the built in close button could be <Link
, but then that pushes this same problem down into the form. Now the form needs to setState and render a <Redirect
for the Save and Delete operations.
The redirect would go away w/ the form state so it should work, right?
Anyway, like I said, we'll provide a HOC that'll pass router as a prop.
The whole point of setState is to change the state of your app and render something else.
Exactly. But to me the current route (the URL) is effectively a state, it's not rendered itself. It's a state that causes other components to render in a specific way (thanks to react-router
).
We should _"Make the URL your first thought, not an after-thought."_, so why not manipulate the URL first and let the app render accordingly..
By using Just Componentsâ„¢, we have effectively pushed all imperative code up to your app's opinions on state management.
So let's say I have a component where the user can click to any x/y coordinate and this should navigate to a new URL containing the coordinates. How should that be done, then?
Like so?
handleXYClick(x, y) {
this.setState({navigateTo: `/just/a/example/${x}/${y}`});
}
render() {
return (
<Whatever>
<FancyUI />
<SomeInputForm
foo=1
bar=2
baz="whoo
/>
<ImageMap onClick={handleXYClick} />
{ this.state.navigateTo && <Redirect to={this.state.navigateTo} />}
</Whatever>
);
}
Maybe (probably) I'm completely missing something, but if that's the way to do - how is that any better than using an imperative method? In the end, this looks still like imperative style to me and just makes the code harder to read...
This is just a completely fictitious example - I'm just trying to show my point.
Are you using redux? Navigate with dispatch
Yes, I'm using Redux, but since react-router is still in "alpha" state, there is no matching react-router-redux version, yet.
Until we're there, I'm using my own implementation that's based on an react-router-redux fork. That fork does not (yet) give me the necessary dispatch action to navigate, that's why I added my own changes.
I was having a really hard time to figure out how to change the URL in my Redux routerMiddleware()
as I can't use <Redirect>
there.
In the end I gave up and simply did a (really, really ugly) window.location.hash = location.pathname;
hack. It works absolutely fine, but... you know... don't tell anybody...
After all, I really hope that V4 will be released soon and react-router-redux will follow soon thereafter (a great combination!) - then I will throw away that hack and just use that...
Similar issue here. I pop up a modal for social network login and pass it callback. The callback, upon success, needs to then redirect the page to close the modal. The callback function is in index.js
(same as my <BrowserRouter> ... <Match>
) so unsure after social login how to call a redirect?
I tried importing This seems way too difficult for something so trivial in any app. How in a function (not a component) could I leverage the browserHistory
and passing it to <Redirect to='/dashboard'/>
without building a component and calling render() ???
Anyway, like I said, we'll provide a HOC that'll pass router as a prop.
If I want to access the router from the same file I imported it, how can I access it? I have no problem accessing it from sub components called during Match (using context), but from the original file with my routes (where my function for auth lies), it cannot access it to do redirects?
import Match from 'react-router/Match';
import Miss from 'react-router/Miss';
import Link from 'react-router/Link';
import Redirect from 'react-router/Redirect';
import {BrowserRouter, browserHistory} from 'react-router';
const auth = {
uid: null,
authenticate() {
},
authHandler(err, authData) {
if (authData) {
// store in database
// redirect ?????????
}
}
}
// ... followed example from your docs on Redirect(Auth) workflow
// needed to be able to call 'auth' from other components which I passed via props
// since callback from social login (Firebase) performs here, cannot figure how to access router to redirect???
const Root = () => {
return (
<BrowserRouter history={browserHistory}>
{({ router }) => (
<div>
{auth.isAuthenticated ? (
<p>
Welcome! {' '}
<button onClick={() => {
auth.logout(() => {
router.transitionTo('/')
})
}}>Sign out</button>
</p>
) : (
<p>You are not logged in.</p>
)}
<Match pattern="/login" component={Login}/>
<MatchWhenAuthorized
pattern="/dashboard"
render={() => <App auth={auth}/>}
/>
<Match pattern="/" render={() => <Home auth={auth}/>}/>
<Miss component={NotFound}/>
</div>
)}
</BrowserRouter>
)
}
render(<Root/>, document.querySelector('#main'));
It's right there in your BrowserRouter
child callback. Unless there's a bug, that's the exact same thing on context.
Outside of Root, however, no access to it. See above I have some other objects declared in file like you guys did fakeAuth
...
So I perform Match on / which takes me to Home. Within home I have login button and call this.props.auth.authenticate()
which is reference back to the index.js file with routes... it calls authenticate() which calls Firebase facebook login and gives it authHandler as callback.
After Firebase authenticates and executes authHandler, that object does not have access to router to perform redirect.
Thank you for answering so quickly, by the way... I do appreciate. I've been stuck for a couple days just trying to do a simple redirect after login. I want to centralize my auth logic to index because many components may want to call it.
Here is my withRouter
HOC if you need one one now. I'm sure there are optimizations or something that this is missing. I will definitely abandon this implementation when an official one gets added.
import { propTypes as rrPropTypes } from 'react-router'
const withRouter = (Component) => {
const WrappedComponent = (props, context) => (
<Component {...props} router={context.router}/>
)
WrappedComponent.contextTypes = {
router: rrPropTypes.routerContext.isRequired
}
return WrappedComponent
}
You just wrap it around your components and it adds the router
prop
export default withRouter(function DummyExample(props) {
const goHome = () => {
setTimeout(() => {
props.router.transitionTo('/')
}, 1000)
}
return (
<button onClick={goHome}>Go Home in 1 second</button>
)
})
Thanks @AsaAyers but issue is I am not trying to access it from a sub component. I can do that via the this.context.router
in any component. I'm trying to access it in the same js file that I instantiate
The only thing I can think of is add an argument to authenticate(provider, router) that I call from sub components... from them I pass back this.context.router
and then authenticate has to store it so the callback function can access it somehow.
index.js
render( <BrowserRouter><Match '/' render={()=>{<Home auth={someObject}}/></BrowserRouter>)
Then from the Home component I call this.props.auth.authenticate
and centralize my auth logic so any component can use it or check auth status...
the callback function authHandler
that I give to Facebook, cannot access router
ugh! ;-)
same problem here..
Is there another strategy for changing the route in a not-loaded-from-Router component?
I use Redirect inside a wrapped component, but I have no idea how to use it in my root component code :)
@mikesparr You could do something as simple as set a router router
property on your auth
object in your <Root>
component.
const auth = {
uid: null,
router: null,
authenticate() {},
authHandler(err, authData) {
if (authData) {
this.router.redirect(X);
}
}
}
class Root extends Component {
componentWillMount() {
auth.router = this.props.router;
}
render() {
// ...
}
}
ReactDOM.render(
<BrowserRouter>
{({ router }) => <Root router={router} />}
</BrowserRouter>,
document.getElementById('main')
)
Also, There should be a withRouter
higher order component on the way soon, so you could do a similar thing by wrapping <Root>
with that.
@alisd23 I appreciate the tip. I tried and still didn't do what was needed. I ended up using the base.onAuth((user) => {...})
in componentWillMount()
to update local state with uid. Then in the componentWillUpdate()
I check for existence of UID and absence of DB record, and then add the syncState like in COTD demo.
It was grueling but worked and meets desired functionality, better leveraging Firebase's built-in functionality. Thanks so much for suggestion though!
I've been stuck for a couple days just trying to do a simple redirect after login. I want to centralize my auth logic to index because many components may want to call it.
I'm using redux-saga and I have just migrated to react-router v4. Before the migration I used react-router and router-router-redux together. In my auth sagas I can do something like this:
import { take, call, put } from 'redux-saga/effects'
import { push } from 'react-router-redux'
function* login() {
const { payload } = yield take('LOGIN_REQUEST')
const response = yield call(api.login, payload)
// ... login successfully
yield put(push('/profile')
}
It seems react-router v4 don't work with react-router-redux. BTW. according to react-router-redux/issues/454 we don't need router-router-redux anymore. But how can I do the same thing in react-router v4 ?
@vimniky You can write you own version of BrowserRouter component. Extract browserHistory, get methods from there and listen it.
follow #4044
@vimniky Did you ever figure this out?
@alex-mann I am using redux-sagas and I add an onSuccess param to my actions and pass the browserHistory from there. Keeps the redux-saga "clean" and makes testing easier
Example action creator
export const createUser= (user, onSuccess) => {
return {
type: CREATE_USER,
payload: {
user: user
},
onSuccess: onSuccess || null
};
};
Example Dispatch v3
this.props.createUser(user, () => browserHistory.push('/dashboard'))
Example Dispatch v4 (wrapping the component in withRouter function)
this.props.createUser(user, () => this.props.history.push('/dashboard'))
Example SagaFunction
try{
const response = yield put(Api.post, '/user', action.payload);
yield put({type: 'CREATE_USER_SUCCESS', user: response.user});
if(action.onSuccess) action.onSuccess()
}catch(e){}
Solution below:
route:
if (isShowModalWindow) {
return < ModalWindow location={location} />
}
return null;
}} />
Solution: when I want to CLOSE or SUBMIT Modal window,
I receive SuccessSubmiting from redux-form in my props:
componentWillReceiveProps(nextPorps){ //
if (nextPorps.SuccessSubmiting)
this.closeModal.handleClick(e);
}
< Link to={state: {modalWindow: undefined }
ref={(node) => {
this.closeModal = node;
}}
>
< Button />
< /Link>
@PiereDome This is my use case as well. I only want to change the route on success. I don't understand your v4 solution. What is "this"? Here's what my code looks like:
property_actions.js
import { history } from '../entry'
let propertyCreated = ({ ID }) => {
history.push(`/updateproperty/${ID}/amenities/`)
}
export function createProperty(property) {
API.createProperty(property, propertyCreated, error)
}
This works with hashHistory, but not with browserHistory. This isn't in a component, it's in my property_actions.js
file.
@ryanflorence
Are you using redux? Navigate with dispatch
I like the idea of using redux to navigate. How would I do that? I need to be able to navigate as part of a success callback to an API call.
Most helpful comment
That's fine, but don't make it _impossible_. Sometimes imperative code is the right tool for the job.