-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[added] getAsyncProps for route handlers #396
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
Conversation
This commit adds the ability for route handlers to load props they need for a given route, optionally asynchronously. Props loaded in this manner cascade from route handlers further up the hierarchy to children, such that props loaded by children take precedence. getRouteProps should return a "hash" of props, the values of which may either be immediate or deferred using promises. As promises resolve, forceUpdate is used to re-render the <Routes>. If no props are promises, the operation is fully synchronous and route handlers will have all the props they need on the initial render. As implemented, this work should satisfy the use cases in #319, #261, #314, #374, and (indirectly) #262.
Alright, I've been screwing around with this over at https://github.com/rackt/react-router-mega-demo. It works great! Except, one thing, it can only resolve one promise and then it calls back. But I must get some rest. I'm going to spend some time making a release instead of working on this tomorrow morning, not sure when I'll get back to it, but hopefully we can get this all working this week :D |
Also, the HTML is getting completely blown away, but I really barely know much about server-side rendering in react, so maybe I'm just not doing something I'm supposed to. |
Not sure what you mean by this. |
But notice that we only get the tacos: |
In order to not blow away the HTML, all the props to the routes have to be the same. This changes our approach a bit. |
Or change the approach completely ... In order to not blow away the DOM when the client code lands, we have to have the props before creating our route config: <!-- must be the same props on the server and client -->
Route(props); In a nutshell, on the server we need to
You define your routes in one place, but then we have to go update the ones that are actually used for the client to have the same data-checksum. |
I'm unclear if we will be able to get the proper checksum w/o putting the properties directly on the |
that javascript scope will get you EVERY TIME if you let it
I just skimmed this PR, so I might be missing something, but I don't think this is a substitute for #374. #374 allows the Do you expect var oldGetRouteProps = routes.props.getRouteProps;
var getRouteProps = function () {
var result = oldGetRouteProps();
result["settings"] = settings;
return result;
};
routes = React.addons.cloneWithProps(
routes,
{
"getRouteProps": getRouteProps
}
); Otherwise, I don't think this can replace #374. |
(The props don't have to be the same; the generated HTML does.) |
fdb2078
to
39fa29e
Compare
39fa29e
to
253bc1a
Compare
@appsforartists instead of |
@rpflorence I feared that. I don't think this is a replacement for #374. |
Same effect, different location. Is it simply the API that is unappealing On Monday, October 13, 2014, Brenton Simpson [email protected]
|
Technical difference: #374 enables a server to serve an arbitrary set of routes that it knows nothing about, but it can pass data down for them to consume if they need to. The handlers don't have to be written any differently or implement any magic static methods. In theory, someone could compose a more opinionated library on top of #374 that would interact with handlers the same way as ReactRouter (by passing specially named In my particular use case, I've put together a server (that I plan on open-sourcing when I get the go-ahead from legal) that will take a set of routes and turn them into an application that can render on both the client and server (and be live-edited with react-hot-loader). The consuming app just needs to pass in a set of Routes, and I pass environmental data down into them as props. I have a prototype of this written on top of #374, and I wouldn't be able to port it over if this lands and #374 doesn't. |
@appsforartists I've been having a thought that smells like what you're saying but how does your server know what data to go get if it knows nothing about your routes? |
@rpflorence, in this case, it's all dynamic. My app is structured Some of the settings are generated in the individual demo instances and some are generated within their constructor (here called All three demos are being served by a single Node instance because I only have access to a single load balancer; the server itself routes based on the request's |
The |
@appsforartists if you simply had |
@rpflorence Yup. That's #374. |
@mjackson I don't think we should merge the props from the parent into the child (and not just because it breaks everything) but instead implement One of my primary goals with the router is to be able to move view hierarchy around w/o having conventional data flow fall apart. If props get merged from parent to child, you could move a handler and then not have the props you thought you did anymore. If you need to pass props from a handler to a child, you can do that easily and more idiomatically with Finally, if we implement |
Sorry, maybe I am a little lost. You're right that in a running application, the state is kept by the stores. But let's move to what gets dehydrated/rehydrated on the way from server to client. Because if I understand it correctly, you suggest that on the server I dehydrate the return value of Anyway, it's just one way to do things. The important thing is that your proposed API will let me do that. But why wouldn't willTransitionTo: function(transition, params) {
var store = context.getStore('ArticleStore'); // context from closure
if (!store.hasArticle(params.id)) {
transition.wait(context.callAction(loadArticle, params.id));
}
} This should just wait until |
@tobice 👍 This is the API I would love for server rendering. |
@tobice I use completely same approach as you: I use |
@th0r And what about that closure trick @rpflorence suggested? It's not nice but that should do the job right? |
And another one nice thing about using |
I think there is a huge overhead in this "closure" approach: for each request you are building the whole routes tree and creating new copies of component classes! |
So you are worried about performance? I cannot judge how demanding this recreating would be but it's pure JavaScript so it should be fast in general. I would guess that in a real-life application the bottleneck would be something else, like any I/O operation. But I would definitely prefer if I could just get a |
@th0r I think you misunderstand closures. If you look at how @rpflorence's example is written:
That component is what's being used on every request. |
@appsforartists actually I need every request to have its own context. Therefore I have to recreate all routes with the new context in each request (call |
@tobice Maybe not. There's an experimental feature in React, also called You would create a root handler whose only job would be to close-over the Dispatchr context. It would then put that into a React Here's an example of using contexts. |
Something like this: /**
* @jsx React.DOM
*/
var Home = React.createClass(
{
"contextTypes": {
"dispatchrData": React.PropTypes.object.isRequired
},
"render": function () {
return <div>
<h1>
Hi from the home screen!
</h1>
{ /* use this.context.dispatchrData however you normally would */ }
</div>;
}
}
);
var curryRootWithDispatchrData = function (dispatchrData) {
var Root = React.createClass(
{
"childContextTypes": {
"dispatchrData": React.PropTypes.object.isRequired
},
"getChildContext": function () {
return {
"dispatchrData": dispatchrData
}
},
"render": function () {
return <this.props.activeRouteHandler />;
}
}
);
return Root;
};
var routes = (
<Routes location = "history">
<Route handler = { curryRootWithDispatchrData(dispatchrData) } >
<Route handler = { Home } />
</Route>
</Routes>
);
React.renderComponent(
routes,
document.body
); |
Notice how |
I've heard of this React context but I also remember something about it being experimental and not implemented after all. Anyway, the thing is that it doesn't really solve my problem because it's tight to an instance whereas the |
@appsforartists, believe me, I know what closures are =) // main.js
var context = makeContextHoweverYouDoIt();
var routes = createRoutesWithContext(context);
React.render(routes); I think he meant, that this code should be executed per-request, not once. // react-router-middleware.js
var Router = require('react-router');
var getFluxInstance = require('./flux-factory');
var routes = require('./routes.jsx');
module.exports = function (req, res, next) {
var flux = getFluxInstance();
var context = {
flux: flux
};
Router.renderRoutesToString(routes, req.path, context)
.then(function (result) {
var statusCode = 200;
// `transitionTo` calls during server-side rendering should immediately stop it
// and resolve `renderRoutesToString` promise with the `redirect` flag set
// and with the new path set as `path`.
// Same thing should happen if `Redirect` route was matched.
if (result.redirect) {
return res.redirect(result.path);
}
// if `NotFoundRoute` was matched, `notFound` flag is set.
// `html` prop contains rendered html string.
if (result.notFound) {
statusCode = 404;
}
// Wrapping generated html with some layout and sending it to the client
// with serialized stores state.
// We can even get `statusCode` from some store if we need to.
return res
.status(statusCode)
.render('layout', {
// We can access any data we need from stores
title: flux.stores.PageTitle.getTitle(),
html: result.html,
// Put serialized data in some global variable and
// call `flux.fillStores(window._storesData)` on the client
// or do some more complex things with it
data: flux.serializeStores()
});
})
.catch(function (err) {
// Some kind of error during rendering
// (transition aborted, transition promise rejected etc.)
// You can store any additional info either in `context` object,
// or in reject error, or in stores and do whatever you want here.
next(err);
});
}; // flux-factory.js
var Flux = require('some-flux-implementation');
var stores = require('./stores');
var actions = require('./actions');
module.exports = function getFluxInstance() {
var stores = {
PageTitle: new stores.PageTitleStore(),
CurrentUser: new stores.CurrentUserStore(),
ViewItem: new stores.ViewItemStore()
};
return new Flux(stores, actions);
}; // routes.jsx
var App = require('./handlers/App.jsx');
var ViewItem = require('./handlers/ViewItem.jsx');
module.exports = (
<Routes location="history">
<Route handler={App}>
<Route name="view-item" path="/item/:itemId" handler={ViewItem}/>
</Route>
</Routes>
); // handlers/App.jsx
var App = module.exports = React.createClass({
statics: {
willTransitionTo: function (transition, params, query, context) {
// Fills `CurrentUser` store
transition.wait(context.flux.actions.getCommonPageData());
}
},
render: function () {
/* ... */
}
}); // handlers/ViewItem.jsx
var ViewItem = module.exports = React.createClass({
// Mixin, that subscribes to stores and gets handler state from them
mixins: [StoreWatchMixin('ItemView')],
statics: {
willTransitionTo: function (transition, params, query, context) {
// Will run only after App's `willTransitionTo` hook resolve,
// so we already have `CurrentUser` store filled
var flux = context.flux;
if (!flux.store('CurrentUser').user) {
// Router should stop server-side rendering at this point
// and call `renderRoutesToString` callback with `redirect` flag set
return transition.redirect('login');
}
transition.wait(context.flux.actions.getDataForViewItemPage(params.itemId));
}
},
getStateFromStores: function () {
// `context` should be provided by router using react context feature
var ItemView = this.context.flux.stores.ItemView;
return {
/* ... */
};
},
render: function () {
/* ... */
}
}); |
@th0r 👍 that's it. The question is whether we can convince @rpflorence to implement it for us. In my opinion it's not an unreasonable request. What actually works right now is this: getRoutes(context) {
return(
<Routes location="history">
<Route handler={App} context={context}>
<Route name="view-item" path="/item/:itemId" handler={ViewItem} context={context}/>
</Route>
</Routes>
)
}
Router.renderRoutesToString(getRoutes(context), path, function(...) {..}); Although it might resemble it, this is not the closure workaround. This is simply providing default props for the root components. That's something which is very natural for React and we do it all the time: React.renderComponent({ ...props... }, mountPoint); The default props provided for the root component are something that originated outside of the React hierarchy (or outside of the route hierarchy in this case). The existence of these props is pretty justifiable. Unless we want React to take care of everything, we need some entry point. After all, Now quoting the documentation for
So at this point, those extra properties are passed to handler instance. Perfect. So why don't we just pass them to hooks (like |
Okay, now I really don't want to get pushy or anything because I really do appreciate what you do... but could you consider changing this line to handler.willTransitionTo(transition, match.params, query, match.route.props); Because that's all I need to get full server-side rendering just the way I described it in my initial comment (I just positively tested it). And this doesn't seem like a change that could break something (but of course, I'm not that familiar with the code). |
@mjackson Oh yes, you're right. Sorry, it's hard to keep up with all the stuff going on around. So consider this as an argument why to do it this way. I'm curious about this better solution, but as far as I'm concerned, this solution would be good enough for me and would let me do things my way. Maybe there's only one thing. If I use Actually, if I think about it, I could implement it just using a smart mixin (assuming that I can define statics in a mixin), so its not a top priority. |
Would it be possible to opt out from having Edit: A minute later I found out that the callback gets the data as the 4th parameter. I guess my comment about opting out still stands since. It's not a mayor annoyance but it's rendering a script tag I (and probably others) don't need. |
Another thing, 424bda1 changed behavior of getAsyncProps(params) {
return {
me: __SERVER__ && context.getApi('ProfileApi').getMe(),
player: context.getApi('ProfileApi').getById(params.playerId),
};
} What would you suggest as an alternative for scenario like this? |
Thanks for messing with this branch everybody. It has revealed that we've gone a bit too far with our abstraction by hiding the creation of your views from you. @mjackson and I are working on some new code we hope to push up to a branch this week that will solve a lot of use-cases around handler props and server-side rendering. It's done by us getting out of your way, rather than some new API or convention like We aren't saying too much about it right now because the best way to get a thumbs up or thumbs down on an API is to have a working prototype. Can't wait to get everybody's feedback on it, we are going to work hard to get it up quickly :) |
It's pretty damned cool that even though we're experimenting on the JavaScript frontier, there are half a dozen people eager to give feedback on all these APIs (and that it's all based on the work of two guys in their spare time). Cheers all around. I love this project. |
@appsforartists thanks :) You, in particular, are going to love the new stuff. I should just stop talking and write some code. |
👍 agreed, this is a great example of open source right here. Appreciate all your guys' work. |
Thanks, can't wait to try it out. |
I'll stand guard outside your door to make sure no one disturbs you while you write. 💂♂️ |
Sounds like a job for the Night's Watch 💂♂️ 💂♂️ |
Stuff to do for server rending integration:
activeRouteHandler
to work again<Route/>
to not wait for data to load (opt in when you expect it to be slow)This commit adds the ability for route handlers to load props
they need for a given route, optionally asynchronously. Props
loaded in this manner cascade from route handlers further up
the hierarchy to children, such that props loaded by children
take precedence.
getRouteProps should return a "hash" of props, the values of
which may either be immediate or deferred using promises. As
promises resolve, forceUpdate is used to re-render the
<Routes>
.If no props are promises, the operation is fully synchronous
and route handlers will have all the props they need on the
initial render.
As implemented, this work should satisfy the use cases in #319,
#261, #314, #374, and (indirectly) #262.