-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add onChange on <Route> #2547
Comments
I could use this make my routes less coupled to my authentication logic. What I have now looks like this: const currentUser = // ...
const authorize = (authConfig) => (nextState, replaceState) => {
if (!currentUser)
replaceState(null, "/SignIn");
if (!currentUser.isAuthorized(authConfig))
replaceState(null, "/SignIn");
};
const routes = {
path: "/",
childRoutes: [
{
path: "abc",
component: ABC,
onEnter: authorize({roles: [a, b, c]}),
childRoutes: [
{
path: "admin",
component: ABCAdmin,
onEnter: authorize({roles: [c]})
}
]
},
{
path: "xyz",
component: XYZ,
onEnter: authorize({roles: [x, y, x]})
},
{
path: "foobar",
component: FooBar,
onEnter: authorize({roles: [foo, bar]})
},
]
} And with const currentUser = // ...
const authorize = (nextState, replaceState) => {
if (!currentUser)
replaceState(null, "/SignIn");
let index = nextState.routes.length;
while (index--) {
const authConfig = nextState.routes[index].authorize || {};
if (!currentUser.isAuthorized(authConfig))
replaceState(null, "/SignIn");
}
};
const routes = {
path: "/",
onChange: authorize,
childRoutes: [
{
path: "abc",
component: ABC,
authorize: {roles: [a, b, c]},
childRoutes: [
{
path: "admin",
component: ABCAdmin,
authorize: {roles: [c]}
}
]
},
{
path: "xyz",
component: XYZ,
authorize: {roles: [x, y, x]}
},
{
path: "foobar",
component: FooBar,
authorize: {roles: [foo, bar]}
},
]
} My Edit: Confirmed that putting custom props on the route object are indeed available from nextState.routes, at least for plain route objects. |
I think I'd prefer to follow React's paradigm of just giving you new props whenever stuff changes. |
I think that works for most use cases. The problem comes up for something like an auth flow - If I'm going from e.g. As it stands, that's not possible; it would be nice if it were. |
Can you show some code to help me understand how onChange helps you do On Fri, Nov 13, 2015 at 2:33 PM Jimmy Jia [email protected] wrote:
|
Contrived example: function authPage(nextState, replaceState) {
if (!auth.mayViewPage(nextState.params.pageId)) {
// In practice maybe this hits a remote and is async.
replaceState(null, '/jail')
}
}
const secureRoute = (
<Route
path="/pages/:pageId"
onEnter={authPage}
onChange={authPage}
/>
) The reason you'd want to do this is because if you try to check this in |
@taion yes I need something like this ( |
@beeant Can you describe your specific use case and why it needs to be handled with a route hook rather than e.g. |
@taion The structure of my codes using redux following various example repos on github according to my needs and preferences. So I followed the way of loading async data using decorator like in https://github.com/coodoo/react-redux-isomorphic-example/blob/master/js/utils/FetchDataDecorator.js It works fine, but it just doesn't work for my filtering feature. this.history.pushState(null, "/search", {filters:filters}); expecting the onEnter will be re-executed with new search query when there is a change on the query string, but it doesn't. I'm currently upgrading my whole app from react 0.13.x and react-router non version 1 → react 0.14.x and react-router 1.x, The Router.run used to be called on query string change. |
It's not that it can't be done with a component's What I have now is an entire component for auth called Authorizer: const Authorizer = React.createClass({
propTypes: {
children: PropTypes.node.isRequired,
routes: PropTypes.array.isRequired,
},
contextTypes,
componentDidMount() { this.checkAuthorization(this.props.routes); },
componentWillReceiveProps(nextProps) { this.checkAuthorization(nextProps.routes); },
checkAuthorization(routes) {
if (!routesAreAuthorized(this.context.user, routes))
this.context.history.replaceState(null, "/SignIn", {ReturnUrl: window.location.href});
},
render() {
if (!routesAreAuthorized(this.context.user, this.props.routes))
return null;
return (
<span>
{this.props.children}
</span>
);
}
});
// then in App.render...
render() {
const routes = {
component: Authorizer,
indexRoute: {
component: Layout,
indexRoute: {component: Home},
childRoutes: [/* ... */]
}
};
return <Router routes={routes}/>;
} Whereas if we had render() {
const user = this.state.user;
const authorize = (nextState, replaceState) => {
if (!routesAreAuthorized(user, nextState.routes))
replaceState(null, "/SignIn", {ReturnUrl: window.location.href});
};
const routes = {
component: Layout,
onEnter: authorize,
onChange: authorize,
indexRoute: {component: Home},
childRoutes: [/* ... */]
};
return <Router routes={routes}/>;
} The latter example is much clearer since I can't avoid extra component hackery. |
I think @jimbolla's case is sort of the clearest illustration for why this helps. It's similar to the one I outlined in #2547 (comment) and shows the current next-best alternative. It's nice to be able to just handle these kinds of things in route matching for changing a route's parameters the way we do for entering a new route, since it lets us save a lot of code in user space for blocking rendering of e.g. permissioned views that should not be seen. I wonder if |
I found a work around for my problem using componentWillReceiveProps() {
if (nextProps.location.search !== this.props.location.search) {
fetchDataPromise(nextProps.location.query, nextProps.params, this.props.dispatch);
}
} |
I think I need to spend some more time with this but it seems to me Is the problem that
|
The idea would be to deal with query changes, but given that we're currently discouraging data fetching in |
I'd really like to see something like this hook if only for better clarification and organization in our app. First, its confusing to have to define route specific setup/param change/teardown logic in more than one place. It makes it hard to know where to look for certain logic, and where to place that logic in the first place. Having all the setup and tear down in Second (and related to 1) when a route renders more than one component there isn't always a clear, canonical, component to place responses to param changes. For instance if a page is rendering a Content and a Toolbar component, which one should respond to route param changes? This is especially true in a flux environment, such components are listening to same state, but don't share a common parent component that could listen an propagate that state. Even more so when some app state needs to change in response to the param change, but isn't otherwise related to any particular component. It would be great to complete the API symmetry here and I think it isn't too much of a stretch right? onEnter and onLeave can also be defined in terms of Handler lifecycle methods, but there is clearly value in having them on the Route itself, for us at least the same is true for route changes. |
Hello, could someone tell me (or point to part of discussion), why data fetching with router hooks is discouraged? In my case it's very convenient to have 'dumb' component tree, which just handles data from props and doesn't require custom-made query params change handling decorators or higher-order components calling some props-propagated function on |
Because they won't fire when navigating between the same nodes in the route tree. I achieve this with some decorator functions. You can also use something like async-props. Data fetching code should be colocated with the component that needs it. Putting it in the routes seems like a bad pattern to me. |
@timdorr I'm putting hooks only on leaf nodes, which change every time. Moving data fetching from components here means the very need of decorators/higher order components. That's what I could happily avoid with 'onChange' hook. Another thing, looks like I'm not getting 'they won't fire' part — from which routes are we going to navigate to prevent hooks to fire, especially |
<Route path='users'>
<Route path=':id' component={UserPage} onEnter={dataFetcher} />
</Route> Going from However, if we add an |
How do we feel about this hook? would a PR be a good way to move the discussion forward here? |
We consider data fetching in route change hooks to be an anti-pattern because it makes it difficult to provide feedback to the user that there is an asynchronous operation currently in progress. |
@timdorr speaking of which, my Redux-inspired experience says it's good to have dumb and functional components most of the time, which translates to 'the less lifecycle hooks are involved the simpler app is'. I think we're running circles. It's about decisions, and I think all parties already had shown their cases and understand tradeoffs. What do we need to have a decision for the issue? |
It seems a number of us are on different pages as to what a router should even do. I believe a router should respond to route changes. Which there are exactly two (non-error) valid responses to a route change, both of which need to be elegantly handled - rendering views and redirecting. @nfcampos says “coupling routes with the components they render should be avoided”… I think that’s exactly what different routes are intended for - rendering different views. Which means at some point in your app whether it’s the router itself or a top level component, the current path absolutely does have to coupled to a view. @timdorr says “Its responsibility is arranging what components are on screen at a particular time”. I agree partly - that is part of its responsibility. The other part is deciding to redirect. |
There are some irrelevant points being made here:
I agree 100%, and nothing I suggested would mean contradicting either of those. |
Now one main point of disagreement is whose responsibility it is to load the data that a view needs to render. First, there’s the part where you should be able to look a component and know what data it needs. as @timdorr puts it: “As for data fetching, I think the contextual relationships are important. Establishing those relationships declaratively is important”. I agree with that, but I can’t help but wonder… are you forgetting about propTypes? As you put it yourself: “Props are a fundamental thing in React and are the primary way of passing state into a component. But no component should be concerned with where those props came from, just that they exist” exactly - this is props/propTypes are for! Why not use them to declare your data needs? One of the points against using the router to fetch data is that it would clutter up the router. That’s where some basic project organization comes into play. If you were to use routers to fetch data, that doesn’t mean you have to do it in a cluttered, unorganized way. You don’t even have to do it all in the same file. Even if the router triggers the data fetching for a view, it doesn’t have to be the thing actually hitting the db or making the ajax call. For my case I would use the router to trigger a single redux action which would fetch all the data it needs. It could even be the router that triggers a static fetchData method on a component if you really want to go that route (no pun intended). As a side note, the organization aspect is actually another thing I don't like about react router - there's really not any outstanding way (it is possible) of breaking the router out into multiple files. Like Express for example - very flexible in organization/architecture. One thing I’d like to point out is that using the router to fetch data is a very common paradigm that many developers are already used to. Look at Express, Rails, etc. All that said, my number one reason to use routes to trigger data fetching is because of the other thing you have to do in routes - redirecting. Let’s contrive up an example… Let’s say the user can view /users/batman but does not have permission to view /users/superman. Let’s also say that the only way to learn this information is to make an http call to fetch the superman data and it responds with a 401. Also, you don’t make this call until you need to render the superman view. Now a user navigates to /users/superman; a call is made to fetch superman; it responds 401; you need to redirect. So now you need to perform a redirect from inside a component as the result of going to a new route. Do you see the problem yet? On the client, it’s some rather ugly logic, and it’s happening in an odd place - the view (poor separation of concerns). On the server it’s even worse - it’s actually not even possible to respond with a redirect from a component unless you cause a side effect to happen. Which you could do, but why? There’s a really perfect place that’s been around for quite some time to handle this - the router. Maybe just not… react router. |
Now back to the problem at hand - It's been brought up before and it will continually be brought up again - because it's a deviation from how most other routers work. Personally, I could even see onEnter being triggered each time there's a change - it's how the browser itself treats it. But anyways, I'll happily take an Even if we disagree on how applications should be architected, why actually prevent me from architecting the way I want to? I've made some points, and some disagree which is fine, but I think there's enough validity to my points to justify an |
Let's get back on topic, please. We are building the router in such a way as to match the best practices we've picked up from extensive experience in building these applications, and we will give priority to practices that we consider to be good. You can choose to take advantage of this or not. Regardless, discussion of architecture here is cluttering up the issue. @ryanflorence, what do you think of @jquense's points in favor of adding |
Yes, the topic. I’ve made a point in favor of It boils down to: without I too would love to hear more opinions on the matter. Another vote to have @ryanflorence or @mjackson chime in. |
@taion Clashing ideas does not make one or the other of us right or wrong. I think that thoroughly hashing out the differences between your own ideas and others that challenge them is how end products are going to be the best that they can be. Take-it-or-leave-it is a little harsh. |
poking around at a PR now...I am not quite sure how to fit an onChange hook in with onEnter. Since onEnter is called on param changes there would be some overlap between the hooks, unless we make onChange only fire on query changes. To my mind the most obvious API is to parallel the lifecycle hooks, with enter == mount, change = wrp, leave = unmount. But there is no path to that without some sort of backwards incompatable change, which (I assume) disqualifies that approach. Personally I don't find the overlap to be a problem conceptually, but maybe it doesn't make sense? |
Trying to convince us that a proposed feature makes an anti-pattern easier is not a good path toward getting that feature added. I think the most expedient way to go about this is to just fire This neatly mirrors how |
it also occurs to me that it could be called param changes and not overlap if the presence of an onChange handler is used as an opt-in for that behavior. It shouldn't break anyone since ther isn't a handler there now, but it does potentially require a refactor if you do want to add in an onChange... Maybe I'll just stick with query changes for the time being and we can discuss it more in the PR |
Not just query changes but also child route changes too. The confusing thing is that param changes also lead to I feel like the straightforward way to do this is just to add that missing last category to |
@taion I've made many points on the stance that it is in fact not an anti pattern, and that the real anti pattern is having the component fetch its own data rather than simply declaring its data needs. Instead of addressing these points you have resorted to saying you're experienced and assuming I am not. |
Yes, and that's exactly what both async-props and react-router-relay provide – but not in transition hooks, which is not where the actual logic should run. |
@tybro0103 That is most definitely an anti-pattern. We're not building a data fetching library, we're building a router. These hooks are only meant to affect the behavior of the router itself, not outside systems. But let's drop this, as that's not the point of this API. The point of this is to allow us to affect router behavior when routes simply change, rather than only when they come into view or are removed. @taion & @jquense I think, given that a param change is consider a route leave-enter event (triggering any |
I don't think we need the architectural discussion here – it seems obvious from an impl perspective to just run Most straightforward implementation-wise, easiest to explain, &c. |
@jquense Is that the approach you've taken thus far? It sounds like we just need another return from |
Yeah I have more or less that implementation sitting here. minimal, straightforward. I ran out of time to send something today, but should finish it up tomorrow |
Just adding my support for this enhancement. I have a use case where we need to add something to the URL if it is not specified and another one where we want to clear things out from the URL. It is just so much nicer to be able to do this while transitioning so that we can avoid the whole "load with wrong format, check a lot of things and then do a redirect" logic that otherwise is required. I understand that this might go a little against principles of React but I believe it does not go against the principles of what a router is responsible of. @taion, have to commend you for your willing to improve on this. Thank you for giving us mortals a voice :) |
Until react-router implements onChange on a Route object, you can do this:
Credits to @dominictobias |
I'll +1 this addition. To me, We already have |
I think that's the strongest argument – if we have |
Continuing from #2332 and also potentially relevant to #2546.
#2122 originally noted that there was no way to observe changes to query params. We attempted a fix with #2137, but it was incorrect and had to be reverted prior to 1.0.0.
To implement this correctly, we'd need to add an
onChange
(or equivalent) hook on<Route>
that gets called when something about the<Route>
changes, to allow users to respond to things like query or URL parameter changes.The text was updated successfully, but these errors were encountered: