Giter VIP home page Giter VIP logo

Comments (157)

ryanflorence avatar ryanflorence commented on March 28, 2024

Yes, absolutely. We have plans that will probably happen this week.

Sent from my iPhone

On Jun 29, 2014, at 12:57 PM, Simen Brekken [email protected] wrote:

Route.spec.js contains a commented out test for server rendering but it seems, server rendering would at least require exposing findMatches in the main module.

Any plans for reintroducing server rendering support?


Reply to this email directly or view it on GitHub.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

There is a bunch of discussion in #38 that is relevant to server-side rendering. Let's continue that discussion here.

from react-router.

phated avatar phated commented on March 28, 2024

Looking forward to this. Started digging through old issues and the code, but not sure where everyone is planning to start. Willing to help in any way and usually on IRC 👍

from react-router.

th0r avatar th0r commented on March 28, 2024

It throws an error in Node during initialization:

/Volumes/Work/new-project/node_modules/react-nested-router/modules/stores/URLStore.js:6
  hash: (window.addEventListener) ? 'hashchange' : 'onhashchange',
         ^
ReferenceError: window is not defined
    at Object.<anonymous> (/Volumes/Work/new-project/node_modules/react-nested-router/modules/stores/URLStore.js:6:10)

It's predictible, because there is no global window there.

@rpflorence, could you please inform about the status of server-side routing support?
I think, that your routing lib is the most convenient and I want to build my "isomorphic" application using it, but unsupported server-side routing stops me...

from react-router.

mjackson avatar mjackson commented on March 28, 2024

The major blocker for server-side rendering at this point is coming up with a sane strategy for fetching data that works on both the client and the server.

Requirements are:

  • It must be asynchronous
  • It must support rendering the view before the data has arrived, for components that want to display "loading..." UI on the client
  • It should also support synchronously passing in data, for testing

Prior art/ideas:

from react-router.

phated avatar phated commented on March 28, 2024

@mjackson ugh, react-async is a pile, don't make people use threads to fetch data.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@phated the fibers stuff is stupid, agreed. But I really like the simplicity of getInitialStateAsync. You don't need threads to pull it off.

from react-router.

phated avatar phated commented on March 28, 2024

@mjackson I think the crux of async server rendering is that React.renderComponentToString is a sync operation so async things can't happen inside it, leading him to use threads. I think fetching async and passing the data into the component as initial props (sync) is the only way to do server rendering.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@phated But he has ReactAsync.renderComponentToStringWithAsyncState which takes a callback, so fibers shouldn't be needed.

Right now I'm thinking:

  • Route#getInitialAsyncState for components to fetch async state. If routes declare this they are asynchronous. If not, they're sync.
  • this.props.initialAsyncState as a way for test code to manually pass in async state.
  • Router.renderComponentToString Which is basically an async-aware version of React.renderComponentToString.
  • Possibly Route#componentWillReceiveAsyncState as a lifecycle hook for components that need to know when they're about to receive some state from getInitialAsyncState. This would only be needed if a component needs to setState in response to some asynchronous state. Not sure if this ever is a good idea, but seems like it could be useful.

If components need to display "loading..." UI, they can check for this.props.initialAsyncState or just check for this.state.someAsyncProp directly.

from react-router.

phated avatar phated commented on March 28, 2024

Sounds like a good plan. I think I actually have a place in current code where I would need the Route#componentWillReceiveAsyncState lifecycle method.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@rpflorence @ericflo @petehunt Would love to get your thoughts here.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

@mjackson yeah, that's pretty much exactly what I'm thinking too, minus the lifecycle hook, though that is compelling. I think we should do everything but that, start using it, and then see if we ever think "shoot, that lifecycle hook sure would be nice right now!"

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

@phated can you explain how you imagine using componentWillReceiveAsyncState?

from react-router.

ericflo avatar ericflo commented on March 28, 2024

This all sounds great to me! Agree with @rpflorence that the lifecycle hook seems like an optional extra. Just to be clear, on the client side there'd be no need to switch the renderComponent call from React to Router, correct?

Also, personally I'd name it something other than renderComponentToString if it acts different from React's method of the same name, and I'd remove the word "initial" from getInitialAsyncState, but this bikeshed is yours to paint :)

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@ericflo Yes. You'll still use React.renderComponent on the client. The async-aware equivalent is just for server-side.

from react-router.

phated avatar phated commented on March 28, 2024

Probably should gist or PR this but I want to see if I am even on the same track as everyone's thinking

var App = React.createClass({
  statics: {
    getInitialAsyncState: function(cb){
      setTimeout(function(){
        cb(null, {
          initial: 'state'
        });
      }, 100);
    }
  }
});
function renderComponentToStringAsync(component, cb){
  var done = function(err, state){
    if(err){
      cb(err);
    }

    component.setState(state, function(){
      var html = React.renderComponentToString(component);
      cb(null, html);
    });
  };

  component.getInitialAsyncState(done)
}

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

I think I'd vote for promises over callbacks if we can.

from react-router.

phated avatar phated commented on March 28, 2024

@spicyj I would too, but usually there's more push back the other way, concept remains the same. I am unsure if I captured the thought behind getInitialAsyncState.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@phated yeah, that's the basic idea. I think getInitialAsyncState should be an instance method, similar to getInitialState but async. Is there a particular reason you made it a static method?

Also, I think React will complain if we try and call component.getInitialStateAsync directly on a descriptor.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

getInitialAsyncState() needs to be a function of the route IMO, not of the props. Otherwise you need to actually render in order to know what to fetch which won't work in a sync environment.

from react-router.

phated avatar phated commented on March 28, 2024

@mjackson I was confused about statics, descriptors, etc. I agree with @petehunt that it needs to be on a route, which would be a prop right?

from react-router.

mjackson avatar mjackson commented on March 28, 2024

you need to actually render in order to know what to fetch

Why is that a problem?

from react-router.

petehunt avatar petehunt commented on March 28, 2024

Because you may need to fetch something to know what to render, then you may need to fetch again to render again, etc etc

from react-router.

petehunt avatar petehunt commented on March 28, 2024

This is a fiddle I made a while ago which somewhat mirrors our internal stuff: http://jsfiddle.net/R88XR/

The gist of the idea is that data fetching may only be a function of the route so that data fetching is batched together and render is batched together (synchronously). So there's a static fetchData(route) -> Promise<data> method.

The data you fetch is accessed at this.state.data. If you render on the server, the data is fetched before rendering and passed to the component via an initialData prop (which is copied to this.state.data). If there is no initialData, the component itself fetches on mount.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@petehunt I see. Thanks for sharing the example.

This would be so much easier if React's getInitialState and renderComponentToString were both async..

from react-router.

mjackson avatar mjackson commented on March 28, 2024

The approach in that fiddle works well for that example, but how does it scale to many nested components? Do you have to pass in all data from the top?

from react-router.

petehunt avatar petehunt commented on March 28, 2024

So the way I'd do it is have this mixin on each handler (which I'd rename to "entry point" if I had my way, btw) and return promises instead of callbacks. Run all the promises in parallel and pass their results back in as initialData props.

If a child component needs to fetch data, it can provide its own fetchData() method and its owner can pass it an initialData prop.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

side note: props.activeRoute and handler are two things I'm comfortable
bikeshedding some more.

On Thu, Jul 17, 2014 at 1:34 PM, Pete Hunt [email protected] wrote:

So the way I'd do it is have this mixin on each handler (which I'd rename
to "entry point" if I had my way, btw) and return promises instead of
callbacks. Run all the promises in parallel and pass their results back in
as initialData props.

If a child component needs to fetch data, it can provide its own
fetchData() method and its owner can pass it an initialData prop.


Reply to this email directly or view it on GitHub
#57 (comment)
.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

what about this?

var Course = React.createClass({

  mixins: [DataFetcher],

  statics: {
    fetchData: function(params, query) {
      return course.find(params.id); // a promise
    }
  },

  render: function() {
    if (!this.state.data) {
      // in the client, this will get a chance to render
      return <div>Loading...</div>;
    }

    // on the server, we would wait for the promises
    // to all resolve and set the data
    var course = this.state.data;
    return (
      <div>
        {course.name}
      </div>
    );
  }

});

var routes = (
  <Route handler={App}>
    <Route name="course" handler={Course}/>
  </Route>
);

// server
// give it the routes, the current route, and the current url
Router.renderRoutesToString(routes, url).then(function(markup) {
  res.send(markup);
});

// client
React.renderComponent(routes, document.body);

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@rpflorence Two pieces you're missing:

  • A componentDidMount that calls this.type.fetchData when this.state.data == null, so the client knows how to get data.
  • Some way to pass the current path to the top-level route.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

the mixin would do the first, and I just updated the code for the second as you were typing your message :)

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

updated the code once more to just pass the url instead of route name and params.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

It might be valuable to have some way to avoid re-fetching data when changing the params or query string in a way that doesn't affect the actual fetch request. One way to do this would be to make the API more like the following:

  statics: {
    getFetchParams: function(params, query) {
      return {id: params.courseId};
    },
    fetchData: function(fetchParams) {
      return course.find(fetchParams.id); // a promise
    }
  },

and then the router could compare the old and new fetch params with _.isEqual or similar when the route is updated and skip the fetch if the fetch params haven't changed. Slightly more typing in the simplest cases though.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

In my prototype (that I lost...) fetchData() returned a set of URLs that represented fetches (i.e. {'http://mything.com/users/1': true}). Made it easy to decide when you needed to fetch new data, and also made it easy to dedupe fetches from multiple components.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

@petehunt How does that URL get turned into actual data?

One more thing: you could also make the mixin syntax like this instead of defining magic static props on the component class:

  mixins: [DataFetcher({
    getFetchParams: function(params, query) {
      return {id: params.courseId};
    },
    fetchData: function(fetchParams) {
      return course.find(fetchParams.id); // a promise
    }
  }],

from react-router.

petehunt avatar petehunt commented on March 28, 2024

@spicyj You can inject fetching plugins:

ReactNestedRouter.injection.injectFetchingPlugin(new BackboneFetchingPlugin('Users', UsersCollection));
ReactNestedRouter.injection.injectFetchingPlugin(new FirebaseFetchingPlugin());

// ...

statics: {
  fetchData: function(routeParams) {
    return merge({
      'backbone://Users/' + routeParams.userID: true,
      'firebase://myfirebase/key': true
    }, SomeChildClass.fetchData(routeParams));
}

// You can look up the response in this.state.data['backbone://Users/' + this.props.routeParams.userID] etc

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

@petehunt you're losing me...

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

One more thing: you could also make the mixin syntax like this instead of defining magic static props on the component class:

I wouldn't call a hook "magic" just because it lives on statics and isn't part of react's built in "magic" hooks :P

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

I wouldn't call a hook "magic" just because it lives on statics and isn't part of react's built in "magic" hooks :P

Yep. Your original fetchData proposal is pretty clean but it gets messier if you add more functions like I did. :) Plus I like making DataFetcher a mixin factory like that because it makes it obvious that it's tied to the data-fetching functions. It also makes it harder to accidentally add one piece without the other.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

I think what @petehunt is suggesting is that you could use different data "fetchers" (I would prefer "provider") that know about how to fetch data from different sources. Then, you'd use keys so that anybody else who needs the same data knows it's already been fetched. Kind of like how Ember hangs on to the model in parent routes when switching to a different sub-route.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

Basically whenever the route changes, you can very inexpensively diff the new URLs with the URLs you already have to detect what you actually need to fetch. Unpacking it is a little cumbersome but I think we could put some good sugar on this.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@spicyj This is one time when I really wish React didn't complain about merging props with the same name. We could provide a default impl of getFetchParams that just returns params and let "sub" classes override it.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

@mjackson you could name the default impl _getFetchParams() and have it delegate to a getFetchParams() if it exists...

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

@mjackson Yet another thing that's magically solved if you use my mixin factory idea. :)

from react-router.

ericflo avatar ericflo commented on March 28, 2024

I really like the mixin factory idea, fwiw

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@petehunt Ah, yeah. Good idea :) I really like the idea of being able to mix data from several different sources easily.

@spicyj agreed :) it's a good idea.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

Is it valuable to be able to do two fetches in parallel where the server waits for both but they fill in as they load on the client?

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@spicyj I think so. The code that is executing the fetches should call setState as soon as it can and merge the new data.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

@spicyj I think the more interesting problem is data we want to only fetch on the client but not on the server. Maybe we can just put conditionals in fetchData()?

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

@mjackson I thought the promise returned by fetchData turns into this.state.data when it resolves?

@petehunt Or perhaps a clientOnly: true in the fetch config.

from react-router.

petehunt avatar petehunt commented on March 28, 2024

FWIW, I like the idea of returning a "fetching descriptor" rather than a promise since it's easy to diff and dedupe a descriptor.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

Seems like this conversation is heading toward the router being an intelligent data layer.

Forgive me if I'm being naive, but doesn't the router have to simply supply an asynchronous hook + data property when it resolves and then you can implement all of these other great ideas in the hook yourself?

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@rpflorence +1

from react-router.

mjackson avatar mjackson commented on March 28, 2024

Decided to take a stab at a basic DataProvider. Is there anything that y'all would like to do that you couldn't easily build on top of that?

from react-router.

jlongster avatar jlongster commented on March 28, 2024

Very excited about this, definitely going to try it out when Router.renderToString lands. At first I was wondering why we need to worry about loading data, but after trying some server-side techniques I get it.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

Now that activeRoute is a function, is it possible to determine what components will be rendered without actually rendering? I'm don't think it is.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@spicyj We don't know all the components, but we should still be able to know the route handlers. After we call dispatch() we know which handlers are going to be active next time we render(). Since the data fetching function is a static method of the route handler, I believe that's all we need to know.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

I see, I guess you can't access the props passed from your parent in the data-fetching code. Makes sense.

from react-router.

th0r avatar th0r commented on March 28, 2024

Yet another feature I would like to have here is handling of 404 pages server side.
What I mean here is if browser requests for page /user/:id/profile and there is no such user, server should render page component to string and response with 404 HTTP code. How can we handle this case?
Another unclear thing is redirects in transition hooks on server side: it should just response with 302.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

Hopefully transition hooks will just work. renderToString should wait for those also.

I think 404s are weird for UI. If you didn't get a user, tell the user you didn't get a user, there's still UI you can render and give the user an experience in your render method.

But anyway, seems like this should be fine:

React.createClass({
  statics: {
    fetchData: function(params) {
      return getUser(params.userId);
    }
  }
});

// on the server
app.get('*', function(req, res) {

  Router.renderRoutesToString(routes, req.url).then(sendResponse, function(rejectReason) {
    if (reasonIs404(rejectReason)) {
      send404(res);
    }
    else if (reasonIs500(rejectReason)) {
      send500(res);
    }
    else if (/* etc... */) {}
  });
});

If any of your promises reject, you'll get the reason, and then you can branch or do whatever you'd like.

from react-router.

th0r avatar th0r commented on March 28, 2024

404s are needed for SEO.
Ok, I've got the case with rejected data, but there is another case, when no route matched the url. Is there any way the router can tell me, that he have just rendered 404 page?
And I haven't got your answer about transitions. I'll try to explain, what I mean: say, you have the route /admin and only admins can get there. Our user is not admin and he enters this url in browser.
What should happen? He should be redirected to /login and see the login form rendered.
What do we have in our case? He will see the login form rendered, but the browser will still show /admin url in the location bar.
Or do I miss something?

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@th0r Why don't you use transition.redirect('/login') in your willTransitionTo hook? That way the user will see /login in the URL.

from react-router.

th0r avatar th0r commented on March 28, 2024

@mjackson, are you talking about serverside? What does transition.redirect('/login') do on the server? How can it change user's browser location without sending 302 HTTP code back to the user?

from react-router.

tdreyno avatar tdreyno commented on March 28, 2024

I like the API discussed here, but don't really care/need about the async or 404 stuff. 👍 to getting a simple implementation out and building upon that for more difficult use cases.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@th0r Ideally, yes. We would be able to use the same API on the server as we do on the client, including things like transition.redirect. Ultimately it will be the responsibility of the server to send the 302.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

Alright, I've had a few conversations with @spicyj and @jaredly, and others about this and agree that we should provide a way to allow people to define multiple promises that resolve to different state on the component, not just one big hammer on this.state.data. You want to render any UI you have data for as soon as you have it!

@jaredly and I just rapped out some API here, and I really like it. Please shoot holes through it.

var User = React.createClass({
  statics: {
    dataProviders: {

      // these keys become state, so `this.state.user`
      user: function(params) {
        return getUser(params.userId).then(null, function(reason) {
          // could branch here for server rendering and send 404 if you want
          return { error: true, reason: reason };
        });
      },

      activity: function(params) {
        return getActivityForUser(params.userId);
      }
    }
  },

  componentDidReceiveData: function(key, data) {
    // key is 'users' or 'activity'
    // a chance to set up polling, retry a failed request, etc.
  },

  render: function() {
    // router exposes the promises to the user for inspection, progress, etc.
    var user = this.state.dataProvider.user.isResolved() ?
      <LoadingUser/> :
      <User user={this.state.user}/>;

    // and for the 95% case, just check if you have data
    var activity = this.state.activity ?
      <LoadingActivy/> :
      <Activity activity={this.state.activity}/>;

    return (
      <div>
        {user}
        {activity}
      </div>
    );
  }
});

I really like that the user is required to define the key where the data is going to be set, I like the dataProviders name, its very clear, and doesn't imply "fetching" or anything either, because sometimes you will synchronously return stuff. I just really like it all around, so you're going to hurt my feelings when you tell me its dumb.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

NB: the lack of a mixin. Given that the Router will already need some omniscience about data loading in order for renderRoutesToString to work, it seemed better not to make it seem independent through a mixin.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

We probably still need a mixin to make this all happen, I'd like to just have a RouteHandler that does

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

That does this and other stuff in the future that make route handlers special.

(Sorry for closing, typing in my phone and missed the textbox with my finger)

from react-router.

mjackson avatar mjackson commented on March 28, 2024

I still think we could do it with just one async hook function and regular old state variables.

var User = React.createClass({

  statics: {

    // Since people want to setState as data is received, we
    // provide a setState function that works like the setState
    // they already know and love.
    fetchState: function (params, query, setState) {

      // If you don't need to do anything async, just update
      // the state immediately and you're done.
      setState({
        user: UserStore.getUserByID(params.userID)
      });

      // Or, ignore the setState argument entirely and return a
      // hash with keys named after the state variables you want
      // to set. The values may be immediate values or promises.
      return {
        user: getUserByID(params.userID) // may be a promise
      };

      // Something more complex...
      var streamData = '';

      return {

        user: getUser(params.userID).then(function (user) {
          // For both client and server side, we have a few classes that
          // mean very specific things. NotFound, Redirect, etc. When we
          // get the defaultHandler in the client, we can use it to handle
          // NotFound. We can also handle Redirect using replaceWith. On the
          // server these would be handled by 404 and 30x respectively.
          if (!user)
            return new Router.NotFound('User with id ' + params.userID + ' was not found');

          // We don't reserve the "dataProvider" or "loading" keys in state, or use
          // a special isResolved() method. Just use loading* state variables as
          // you've always done.
          setState({
            loadingUser: false
          });

          // EDIT: We automatically use this value as `this.state.user`.
          return user;
        }),

        // Streaming data! I've actually got this use case right now where
        // I'm using streaming XHR to load images.
        stream: getStreamingData(params.userID, function (chunk) {
          streamData += chunk;

          // Do progress with callbacks, not promises.
          setState({
            streamReceivedSoFar: streamData
          });
        })

      };
    }

  },

  getInitialState: function () {
    // Use state variables to track when data is loaded, like you usually do.
    return {
      loadingUser: true,
      streamReceivedSoFar: ''
    };
  },

  render: function () {
    if (this.state.loadingUser)
      return <LoadingUser/>;

    return (
      <div>Welcome {this.state.user.name}! So far, you've received {this.state.streamReceivedSoFar.length} data!</div>
    );
  }

});

This also doesn't require a mixin. We just use if (typeof handler.fetchData === 'function') to tell if the route needs to be rendered asynchronously or not in renderRoutesToString.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

ooh, I like the way this is going. You're exactly right, we're just after setting state whenever we feel like it. This api lets somebody implement dataProviders the way I described them, but is far more flexible. <3 it.

s/fetchState/getInitialAsyncState/?

Regarding server sent 404s and such, we could maybe give the user a chance to send any arbitrary data to the resolved value, and let them do whatever they want instead of some pre-baked NotFound stuff:

Router.renderRoutesToString(routes, url).then(function(result) {
  // result.html
  // result[userDefined]
  serverResponse.send(result.code, result.html);
});

from react-router.

jaredly avatar jaredly commented on March 28, 2024

@mjackson so that's a very thin layer which solves the server-side rendering question, but avoids caching, data providers, etc. Which I guess is all that's really needed -- the fancier stuff can be handled by a separate lib

from react-router.

mjackson avatar mjackson commented on March 28, 2024

so that's a very thin layer which solves the server-side rendering question, but avoids caching, data providers, etc.

What do you mean? The intent is to provide an API that solves both client and server needs.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@rpflorence Yeah. We should def call it getInitialAsyncState.

Re: not found, we could handle it on both the client and the server with the <Routes defaultHandler> hook we discussed in #112.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

I am leaning towards the more explicit dataProviders syntax because it gives the framework more information about what data you are fetching. I'm not sure, but I think it would allow better caching and other data niceties.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

I think it would allow better caching

Having routes nested inside one another already gives us some level of caching for free. Nested routes may inherit data from their parent routes, and we don't unload parent routes when transitioning between children, so they preserve state.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

With @mjackson API, my example is nearly identical, except that his feels more similar to react's getInitialState and getDefaultProps. Devs just return stuff from getInitialAsyncState; all naming conventions are the same with stuff everybody already knows.

Equivalent to my earlier example:

var User = React.createClass({
  statics: {
    getInitialAsyncState: function (params, query) {
      return {
        user: getUserByID(params.userID),
        activity: getActivityByUserId(params.userID)
      };
    }
  },

  render: function() {
    var user = this.state.user ?
      <LoadingUser/> :
      <User user={this.state.user}/>;

    var activity = this.state.activity ?
      <LoadingActivy/> :
      <Activity activity={this.state.activity}/>;

    return (
      <div>
        {user}
        {activity}
      </div>
    );
  }
});

I am leaning towards the more explicit dataProviders syntax

  • dataProviders syntax isn't any more or less explicit
  • getInitialAsyncState is more idiomatic to what we already know in react
  • you could build in the dataProviders convention with getInitialAsyncState and a mixin if you prefer that API, but you could not go the other way around (like the streaming case).

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

Also, if you need access to the promises you just do what you need to do in getInitialAsyncState, and with a setState function you are free to do nearly anything.

My question is, how do we know when everything is done? Like in the streaming case, how does the router know you're all done for renderRoutesToString?

from react-router.

jaredly avatar jaredly commented on March 28, 2024

@rpflorence I meant object (key, function), which is statically analyzable, instead of a single function. But I think this is fine.

I assumed that the streaming case returned a promise, but also had a callback for incremental update.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

@mjackson you did setState in the success handler of getUserById, if its a promise, can't we just do that automatically?

edit nvm: you did loadingUser, not user, so it appears your API will indeed set this.state.user automatically.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@rpflorence getStreamingData returns a promise that resolves when the stream is finished, so that's how you know when everything is done.

your API will indeed set this.state.user automatically

Yeah, but I made a mistake. The promise needs to resolve to the user object in order to do that, so I need to return user. I've updated the example.

from react-router.

mjackson avatar mjackson commented on March 28, 2024

This API also lets you load data that depends on other async data.

getInitialAsyncState: function (params, query, setState) {
  return {
    post: getPost(params.postID).then(function (post) {
      // I'm sure there's a better example out there, but you get the idea.
      return getComments(post.commentIDs).then(function (comments) {
        setState({
          comments: comments
        });

        return post;
      });
    });
  };
} 

from react-router.

mjackson avatar mjackson commented on March 28, 2024

I meant object (key, function), which is statically analyzable

@jaredly I know the React devs are planning on eventually being able to statically analyze component props (instead of using React.PropTypes), but I haven't heard about any similar plans for this.state or getInitialState. If/when that happens, getInitialAsyncState can follow suit.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

alright, let's :shipit:

React.CreateClass({
  mixins: [Router.AsyncStateMixin],

  statics: {
    getInitialAsyncState: function(params, query, setState) {
      return {
        someKeyThatBecomesState: valueOrPromise
      }
    }
  }
});

I think we still need a mixin so that we can call that stuff on the client in componentDidMount.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

There was some talk of having an asyncDataDidLoad function that was called once one of the promises resolved, with (name, data). Is that no longer?

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

oh right, componentDidReceiveAsyncState(key, data)

I imagine this being used to set up polling, or to retry if the operation failed. Those two use-cases can be easily solved with setState and your own success/failure handlers in getInitialAsyncState though.

I'm happy to still have it, just wish I could think of how anybody would use it...

from react-router.

jaredly avatar jaredly commented on March 28, 2024

My use case is initializing some other state based on the data that was loaded. I guess I can just have a custom success handler in the getInitialAsyncState.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

lets continue this thread (componentDidReceiveAsyncState) over at #138

from react-router.

madebyherzblut avatar madebyherzblut commented on March 28, 2024

Is there a way to pass some context to getInitialAsyncState? In my use case I have a multi-tenant application on the server side which needs access to the tenant object every time it fetches data.

I need something like this:

var ChatSidebar = React.CreateClass({
  mixins: [Router.AsyncStateMixin],

  statics: {
    getInitialAsyncState: function(params, query, setState, context) {
      var channelStore = context.getStore("channel")
        , userStore = context.getStore("user");

      return {
        channels: channelStore.fetch(),
        users: userStore.fetch({status: 'active'});
      }
    }
  }
});
app.get('/chat', function(req, res) {
  var context = createContext({req: req, tenant: req.tenant});
  Router.renderComponentToString(App({context: context}), function(markup) {
    res.send(markup);
  });
});

Other use cases would include code that depends on locale or session data.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

What about something like this?

// current-context.js
var _currentContext = null;

exports.set = function(context) {
  _currentContext = context;
};

exports.get = function() {
  return _currentContext;
};
var Context = require('./current-context');

app.get('/chat', function(req, res) {
  Context.set(createContext({req: req, tenant: req.tenant}));
  Router.renderComponentToString(App({context: context}), function(markup) {
    res.send(markup);
  });
});
var Context = require('./current-context');

var ChatSidebar = React.CreateClass({
  mixins: [Router.AsyncStateMixin],

  statics: {
    getInitialAsyncState: function(params, query, setState) {
      var context = Context.get();
      var channelStore = context.getStore("channel")
        , userStore = context.getStore("user");

      return {
        channels: channelStore.fetch(),
        users: userStore.fetch({status: 'active'});
      }
    }
  }
});

Obviously the current-context.js is too naive for something multi-tenant, but I think it demonstrates the idea that you can use a module to keep track of what the current context is for a request. I also have questions around how your code would work on the client.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

You know how i love globals :) I'd be on favor of having a context that's
managed by the router
On Jul 29, 2014 6:21 AM, "Ryan Florence" [email protected] wrote:

What about something like this?

// context.jsvar _currentContext = null;exports.set = function(context) {
_currentContext = context;};exports.get = function() {
return _currentContext;};

var Context = require('./context');
app.get('/chat', function(req, res) {
Context.set(createContext({req: req, tenant: req.tenant}));
Router.renderComponentToString(App({context: context}), function(markup) {
res.send(markup);
});});

var Context = require('./context');
var ChatSidebar = React.CreateClass({
mixins: [Router.AsyncStateMixin],

statics: {
getInitialAsyncState: function(params, query, setState) {
var context = Context.get();
var channelStore = context.getStore("channel")
, userStore = context.getStore("user");

  return {
    channels: channelStore.fetch(),
    users: userStore.fetch({status: 'active'});
  }
}

}});


Reply to this email directly or view it on GitHub
#57 (comment).

from react-router.

mjackson avatar mjackson commented on March 28, 2024

@madebyherzblut How does your app determine what the context is (i.e. which tenant)?

If it's based on something in the URL (e.g. /:tenantID/dashboard) then you can use params.tenantID to setup context inside getInitialAsyncState.

Otherwise, if it's based on cookie/session data I'd recommend passing the context in through to your view hierarchy as a prop as @rpflorence suggests.

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

A real implementation would manage a set of contexts based on session.

Sent from my iPhone

On Jul 29, 2014, at 10:29 AM, Jared Forsyth [email protected] wrote:

You know how i love globals :) I'd be on favor of having a context that's
managed by the router
On Jul 29, 2014 6:21 AM, "Ryan Florence" [email protected] wrote:

What about something like this?

// context.jsvar _currentContext = null;exports.set = function(context) {
_currentContext = context;};exports.get = function() {
return _currentContext;};

var Context = require('./context');
app.get('/chat', function(req, res) {
Context.set(createContext({req: req, tenant: req.tenant}));
Router.renderComponentToString(App({context: context}), function(markup) {
res.send(markup);
});});

var Context = require('./context');
var ChatSidebar = React.CreateClass({
mixins: [Router.AsyncStateMixin],

statics: {
getInitialAsyncState: function(params, query, setState) {
var context = Context.get();
var channelStore = context.getStore("channel")
, userStore = context.getStore("user");

return {
channels: channelStore.fetch(),
users: userStore.fetch({status: 'active'});
}
}
}});


Reply to this email directly or view it on GitHub
#57 (comment).


Reply to this email directly or view it on GitHub.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

Yeah --- I think it's way out of scope to put together a full context
manager -- that's the job of express or whatever framework. The Router
can just support initialization with some context which get's passed
through to the routes.

On 7/29/14, Ryan Florence [email protected] wrote:

A real implementation would manage a set of contexts based on session.

Sent from my iPhone

On Jul 29, 2014, at 10:29 AM, Jared Forsyth [email protected]
wrote:

You know how i love globals :) I'd be on favor of having a context that's

managed by the router
On Jul 29, 2014 6:21 AM, "Ryan Florence" [email protected] wrote:

What about something like this?

// context.jsvar _currentContext = null;exports.set = function(context)
{
_currentContext = context;};exports.get = function() {
return _currentContext;};

var Context = require('./context');
app.get('/chat', function(req, res) {
Context.set(createContext({req: req, tenant: req.tenant}));
Router.renderComponentToString(App({context: context}), function(markup)
{
res.send(markup);
});});

var Context = require('./context');
var ChatSidebar = React.CreateClass({
mixins: [Router.AsyncStateMixin],

statics: {
getInitialAsyncState: function(params, query, setState) {
var context = Context.get();
var channelStore = context.getStore("channel")
, userStore = context.getStore("user");

return {
channels: channelStore.fetch(),
users: userStore.fetch({status: 'active'});
}
}
}});


Reply to this email directly or view it on GitHub
#57 (comment).


Reply to this email directly or view it on GitHub.


Reply to this email directly or view it on GitHub:
#57 (comment)

from react-router.

mjackson avatar mjackson commented on March 28, 2024

I'd be on favor of having a context that's managed by the router

I think it's way out of scope to put together a full context manager

@jaredly ? Did you change your mind? :)

from react-router.

jaredly avatar jaredly commented on March 28, 2024

=) no, I guess I just wasn't clear with my original comment. Perhaps "context that's passed through by the router" would be clearer.

from react-router.

jaredly avatar jaredly commented on March 28, 2024

I like the code example by @madebyherzblut; context is initialized outside of the router, but the router knows about it and passes it through to the routes and getInitialAsyncStates

from react-router.

ryanflorence avatar ryanflorence commented on March 28, 2024

You can pass "context" down to your routes with route props

// abstraction to be able to do this for all the routes your server responds to
function buildRoutes(context) {
  return (
    <Routes>
      <Route handler={App} context={context}/>
    </Routes>
  );
}

app.get('/chat', function(req, res) {
  var context = createContext({req: req, tenant: req.tenant});
  Router.renderComponentToString(buildRoutes(context), function(markup) {
    res.send(markup);
  });
});

from react-router.

mjackson avatar mjackson commented on March 28, 2024

All that's really left to do on this issue is add a renderRoutesToString(routes, path) method.

React's current implementation of renderComponentToString is sync, but we'll need to make ours async to resolve async state. In v0.8.0 React had a renderComponentToString that had an async API but it wasn't actually async.

Looking through these two implementations, it's not clear to me how to use the ServerRenderingTransaction in a fully async way. For example, can I start a transaction in one turn of the event loop and finish it in another? If not, how should I go about this? If @petehunt or @spicyj has any input here I'd love to hear it.

If you're inclined to ask other questions about server-side rendering that aren't directly related to renderRoutesToString, let's please discuss them in a separate issue to keep this comment thread from growing too large.

from react-router.

sophiebits avatar sophiebits commented on March 28, 2024

No, you can't start and end a Transaction in different ticks. The best approach is probably to fetch all the data async and pass it in in such a way that it the routes can be rendered synchronously after the data is fetched.

from react-router.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.