-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
Some theory and examples for async client-side fetching #2101
Comments
"You're building an app that needs to fetch some data before it renders the routes. " |
Well this problem probably goes beyond react-router. To accomplish this whole thing you need some state management and something that tells you what needs to be updated. |
that depends on architecture yep but the concern seem quite recent; |
@bbnnt wait mate, seems you don't understand. |
Nope I don't fully understand what's going on :/
Right now I'm confused when you point "fetching new state from server on location change", what's happening at this specific moment ? |
|
My concern is about his quote "The site should also be capable of displaying a global loading indicator if any data dependencies are being fetched." |
Practically yes. But in Gmail case. It has to fetch data using Ajax on every page refresh, with server-side rendering, you don't have to fetch anything to render app, as stated above. |
@bbnnt A hypothetical lifecycle of an isomorphic app would go something like this:
@vojtatranta I believe your solution displays a loading indicator but it also renders the new state of the app, without the new data. Correct me if I'm wrong on that. |
@apapirovski nope, I mean exactly same this as you. |
@vojtatranta I understand that you're rendering on the server but in your app, when history listener is triggered, it initiates data fetch. That's all fine but it doesn't stop the new routes from being rendered while the data is fetched. Imagine your app had another route (say About) which rendered a completely different component (StaticPage) from a different state (Static). Now the Gallery component would transition out and the new component would transition in, but it would have no data to show and so would just show a loading indicator. The point of the middleware component is to avoid that blank render because in some apps it's undesirable. That's why I gave the example of GitHub — when you switch between views, it fetches the data while displaying a loading indicator over the current page. |
@apapirovski Ahaaa, |
@vojtatranta I do use Redux. The solution above is generalized for people who don't. Redux or the redux-router don't solve the problem. Think about it like so:
Here are the issues:
Which is why we abstract the solution above the App — it gives us more flexibility and keeps our App pure. TransitionManager can be pulled out of the routes at any time without impacting the rest of the app. |
To those issues:
Otherwise you are right I am curious about any full-featured example with TransitionManager, would be great if you shared one. Thanks. |
Actually this turns out to be very interesting problem. I would like to know some other opinions @knowbody, and maybe someone from redux community... |
I don't have a full redux implementation that I'm happy with but in my head it goes something like:
Something like that. To make it fully featured, I think the dependency probably needs to specify whether it's blocking or not, that is whether the app can render without it. |
@apapirovski a route change that would behave like a promise ? |
BTW, I have a separate implementation of async data fetching in a routing context for It's quite possible that whatever I have in the Relay context will have to stay separate, just because Relay has its own semantics around loading states, and it'd make more sense for |
Oops hit |
@apapirovski Any problems with your solution so far? I am very excited about your solution and plan to use it for my project :) |
I'll ask the same question as @imouto1994 to @apapirovski. Becuase it's difficult to do the same thing that was so easy in the earlier version of react-router, and this seems to be the best solution I can think off... |
No problems with it although I'm using redux-router and have switched over to a middleware approach. |
Are you following the way of |
I have't looked at their implementation but I assume it's similar. I don't think there's much flexibility in terms of how you do it if you're using middleware. Edit: Yeah, it's pretty similar. In mine, I actually created a higher-order component that defines a I also have a separate transitions reducer that stores the current transition state to allow rendering of things like global loading bars / indicators, etc. |
@apapirovski Do you mind sharing about how you tackle the problem that fetched data before navigation complete might crash the current route if it also uses same data with the next route? I find it as one of the main problem for this approach. I am thinking about applying |
Assuming you're using Flux or redux, you could just structure your stores in such a way that data is tied to a key, which could be a param or a slug or something else unique to each path. Then the component would only update when both conditions are met.
|
This one worked for me: import React from 'react';
import Actions from '../../actions/Actions';
import isEqual from 'lodash/lang/isEqual';
const ComponentProvider = React.createClass({
propTypes: {
Component: React.PropTypes.func.isRequired,
initialData: React.PropTypes.object
},
getInitialState() {
return {
isAppHydrating: false,
data: undefined
};
},
// re-render if we're done hydrating
shouldComponentUpdate(nextProps, nextState) {
return !nextState.isAppHydrating;
},
componentWillReceiveProps(nextProps) {
if (isEqual(this.props.location, nextProps.location)) {
return false;
}
if (nextProps.Component.fetchData) {
this.setState({ isAppHydrating: true });
this._addLoading();
this._getDependencies(nextProps);
}
else if (this.state.isAppHydrating) {
this.setState({ isAppHydrating: false });
}
},
_getDependencies(transitionProps) {
transitionProps.Component.fetchData(transitionProps.params)
.then(data => {
this._saveNewData(transitionProps, data);
})
.catch(nodata => {
this._saveNewData(transitionProps, nodata);
});
},
_saveNewData(transitionProps, data) {
if (!isEqual(this.props.location, transitionProps.location)) {
return false;
}
this._removeLoading();
this.setState({
isAppHydrating: false,
data: {
[transitionProps.Component.displayName]: data
}
});
},
_addLoading() {
Actions.toggleLoading(true);
},
_removeLoading() {
Actions.toggleLoading(false);
},
render() {
const { Component } = this.props;
return
<Component
{...this.props}
data={this.state.data || this.props.initialData}
/>;
}
});
export default ComponentProvider; If server rendering or initial render, I'm passing in initialData. |
@apapirovski I know this issue is old, but did you get an example working where the old route hangs out until the promises are resolved? EDIT: this is from shouldComponentUpdate, my bad |
@apapirovski i'm also seeing the app scroll to the top when the transition starts, any ideas on that? |
apapirovski did you get a redux version of this working? I have had success with just setState, but integrating it with redux has proved to be more challenging than I thought. |
for anyone landing on this from google, I have built a module for redux usage that fits the bill! https://www.npmjs.com/package/react-redux-transition-manager |
I've seen a lot of questions about this problem and none of the solutions I've seen out there quite address it in a flexible enough manner for my purposes, so I decided to do this little write-up. If nothing else, hopefully it generates some useful discussion.
Disclaimer: This is an approach that worked for me, I don't claim it's perfect, correct or that it addresses your particular needs.
The problem
You're building an app that needs to fetch some data before it renders the routes. This is simple if you don't care about transitioning down the path before this data is fetched. In such case, you can just use
componentDidMount
(or use Rx to react to something like redux-router state change) to run whatever methods are needed. The server-side aspects of this have been covered endlessly and thematch
solution works well.Let's say, however, that you do care about transitions. I'm currently working on a universal React-based e-commerce website where the data has to be fetched first before the previous components unmount. The site should also be capable of displaying a global loading indicator if any data dependencies are being fetched.
A more relatable example: GitHub project view. The page transitions happen after page data is loaded — while displaying a global loading indicator — not before.
The theory and the potential solution
The react-router render tree always starts with one component, this means that we can nest our actual routes within any number of components that don't have a path on them and that render absolutely no markup (but rather simply
this.props.children
).By creating a top component that sits below the router but above our app, we can use
componentWillReceiveProps
andshouldComponentUpdate
to isolate our app from router changes while letting it go about its business undisturbed.Furthermore, this also means you're free to pass down new properties that will, for example, show a global loading indicator (or progress bar). Because the code relies on
componentWillReceiveProps
, this means that it also won't run on the back-end. It could probably be expanded to support server-side but I already had amatch
based solution that worked for me.Now, for some sample code:
Hopefully the comments made that clear enough to parse. From this base state, you could further extend the class by storing a copy of old
this.props.children
in the app state and usingcloneElement
to generate a version that passes new props down to the component — these could contain things like data loading progress status or simply information about the transition about to happen.Note that if you're not doing server-side rendering then you will need to make sure to update this code to actually run on
componentWillMount
, otherwise you will not get any initial data. You'll probably also want to make other tweaks to enable loading indicators, etc.Further Disclaimer: It's possible this breaks some internals of react-router. I haven't found any issues yet but I haven't used the full api to be completely certain.
The text was updated successfully, but these errors were encountered: