Hey team! Great work with Apollo. I've been working with it now for a month or so with very little issues π
Following up on #153 (comment), hoping to triage some ideas and find a solution for async loading of routes and prefetching data for views ahead of time. CC @amannn from #153, who sounds like was looking for the same thing.
I'm currently working on a large scale editorial site. Lots of data on each page that needs to be fetched. Out of the box, when requesting a new route, I need to use the data.loading prop to return a loading state for all my components:
class MyComponent extends React.Component {
render(){
return this.props.data.loading ? <Loading/> : <Component/>
}
}
So when requesting cold routes, the page is rendered with loading states, and then when the WrappedComponent resolves, the page is re-rendered with the new data. This is usually fine and can handle route transitions pretty easily, but ideally (imo), I could ensure all the data is ready before I render the new route. So instead of request new route > hide current route > show empty new route loading state > populate data in new route it would be request new route > load new route in background > transition from old route to new route smoothly.
I've put together a proof of concept: apollo-prefetch. Basically a HOC that runs getDataFromTree over the requested route AST and blocks shouldComponentUpdate until the promise returns successfully. I'm currently using it as middleware for react-router's render method, which seems to work well.
I was trying to avoid rendering the whole tree twiceΒ βΒ i.e. getDataFromTree β but in order to hit fetchData deeper in the view tree, I think that's the only option. Since we aren't talking real DOM at that point, it's super fast anyway, though I haven't done benchmarks.
My index code looks like this, same example from the apollo-prefetch repo:
import React from 'react'
import { Router, browserHistory, match, RouterContext } from 'react-router'
import routes from './routes'
import { store, client } from './store'
import { asyncMiddleware } from 'apollo-prefetch'
const Root = props => (
<ApolloProvider client={client} store={store}>
<Router
history={browserHistory}
routes={routes}
render={asyncMiddleware({
routes,
client,
store,
onLoad: () => console.log('Loading...'),
onComplete: () => console.log('Load Complete'),
})}
{...props.renderProps}/>
</ApolloProvider>
)
match({ browserHistory, routes }, (error, redirectLocation, renderProps) => {
if (error) throw new Error(error)
render(<Root renderProps={renderProps}/>, document.getElementById('root'))
})
As you can see, this solution requires that you pass client and store to the middleware so that it can be passed as the context param in getDataFromTree(rootElement, context). routes also needs to be available in order for you to utilize the prefetch utility, which is used like this:
import React from 'react'
import { Link } from 'react-router'
import { prefetch } from 'apollo-prefetch'
const callback = (err, res) => {
if (err) return console.warn(err)
console.log(res)
}
export default props => (
<header>
<Link
to="/about"
onMouseOver={e => prefetch('/about', callback)}>
About Page</Link>
</header>
)
Another note: as @amannn mentioned in #153, importing getDataFromTree also imports react-dom/server since renderToString is used in renderToStringWithData. For this to be truly a solution, ideally react-apollo could separate the getDataFromTree and renderToStringWithData definitions.
getDataFromTree at react-apollo/lib/index? Otherwise it's unavailable in the browser export, which makes sense.{ client, store } context from the parent ApolloProvider instead of having to pass the context through asyncMiddleware?I'm pretty new to GraphQL and React, would love any feedback or direction you have! ππΌ
Wow, nice work @estrattonbailey! That feature for pre-fetching data even before a route transition is really nice. Good to see that I'm not the only one who is wondering about such things :).
Some thoughts:
I think the upcoming react-router v4 API will help a bit for this use case. I haven't experimented with it yet, but the main difference is that all the hooks can be implemented as React lifecycle methods.
This has some advantages:
client and store through the regular React context.intl from react-intl that are necessary for the components to be rendered can also be accessed through regular context. Though if you're building a library you need to specify them upfront anyway, so you can supply them to contextTypes.shouldComponentUpdate. I think this should only be used for performance improvements and other logic should be located in a different hook. Maybe with the upcoming React Fiber a sCU-based hook could be problematic as those lifecycle methods could be called at weird times β but I don't really know :). Instead of sCU, you could save the previously rendered element and use it in render until the data for the target component is fetched.It would be interesting to know what led you to this solution.
I initially thought that it's a very web-like way that you stay on the current page until the target page has loaded (mostly known from purely server side apps).
I've thought a bit more about this though and then went for a direct transition with showing a loading state instead. This is what most native apps do, I think. E.g. considering you are on a page and click on a link that opens a modal. I think it would be weird if you'd show a global loading indicator on the initial page and then open the modal after the data has loaded. It would feel much snappier if the modal opens right away.
An interesting example for instant page transitions is medium.com. I think it works really well for them β in particular that content placeholder pattern is really helpful here.
This seems cool!
Is it safe to import getDataFromTree at react-apollo/lib/index? Otherwise it's unavailable in the browser export, which makes sense.
I guess. I hadn't thought we might want to use it on the client, but if so we should factor it so we can. I think it might make sense to make it a separate package from react-apollo in any case, because there's actually not really anything Apollo-specific in it.
Any thoughts on the pattern?
I'm personally of the opinion that showing the user some loading state (and you can often do better than a full-page spinner) is a better UX, but I think there's a case to be made for this kind of "auto-preloading" in other situations (like for instance the mouse over, or even just pre-loading every link on a given page when it renders).
Also, I wonder (esp in RR v4 where routes aren't necessarily available) if there's a way to preload a page given just the App component and a URL. Kind of like you might test it; something like:
<ApolloProvider> // <- this is our real provider so things load into the right store
<MemoryRouter initialEntries={[preloadedUrl]}> // <- this is a memory router so the app thinks we are at a different URL
<App>
</MemoryRouter/>
</App>
@tmeasday I agree its probably time to abstract our server renderer. Where do you want to move it to? Still under apollostack? Under newspring, you, me?
Happy with whereever you like @jbaxleyiii
@stubailo do you have a preference?
I think it's part of a pretty critical feature of React + Apollo that I'd like to invest in, so it would be helpful if it were under the same GitHub org as react-apollo. That would definitely be my preference but I'm open to other ideas as well.
@amannn ah, I definitely need to look into RR v4, that's a great call. I would assume some parity between the current v3 render method and native render lifecycle method, but haven't gotten that far.
Also, it would be really awesome to be able to get client and store from the context directly. I haven't experienced any issues with other contexts, and looks like getDataFromTree does take context into account, but I'm not very knowledgable on working with context so would be interested in what hangups there are and if there's a solution that could be implemented.
Totally agree on the "skeleton UI" bit, and @tmeasday mentions this sort of too. As a patch, I currently have onLoad and onComplete hooks in my setup that can be used with a site loader (like Github). This is what I'm currently doing, but I should probably reconsider showing a blank UI state. That is a really nice UX pattern.
@tmeasday re: routes unavailable in RR v4, interested in investigating what you're saying here, hope to spend more time today on it (I have the day off from work!). I was messing with "auto-preloading" also, adding a preload attribute to my <Link/> components that would queue and request each view sequentially for caching once the app starts up. A little hacky, but I think it could definitely be an option for some people given a more stable implementation.
I imagine regardless of implementation, solving this requires client, store (internal or existing), and an AST for a given route. I think ideally I could rely on react-router to take a route and return me a render tree i.e. match. That, along with a standalone getDataFromTree for the browser, that should give me (and other users) the tools they need to handle prefetching and page loading as they see fit.
Update: perusing the RR v4 page, they mention exactly this use case:
One use case was loading data and waiting to render the next screen until the data landed. With a component, you can save the previous children, render them while loading, and then render your new children when you're done.
That's currently my goal, and @amannn you may be right on avoiding shouldComponentUpdate based on the above π though I'm not totally convinced that it's bad for such a defined use case as this π
@estrattonbailey nice! I was wondering if it is better to block location transition. Example below is for RRv3:
history.listenBefore((location, callback) => {
match({ location, routes }, (err, redirectLocation, renderProps) => (
// fetch data then callback
));
});
@estrattonbailey great work, I stumbled in the same problems with react-router and I haven't had the chance to look into V4 but with the current V3 I ended up using the render prop in <Router> and returning a component which caches and renders the children and calls getDataFromTree every time it gets new props from the router, updating the cache.
https://gist.github.com/dpiccone/2cf8edc2b685137c0e064137383a093a
I wonder if I am missing something or if this is sufficient to achieve the same result.
@tmeasday @jbaxleyiii It would be great if we could break out the server renderer. Let me know if you want me to create a new package for it.
Fine by me
@khankuan yep, that's the pattern I opted for in my library.
@dpiccone nice! Looks like a similar solution. I like that you just use a HOC and RouterContext directly though, smart. If you're looking to preload routes you'll need an interface for match from RRv3.
In the end, for my project we're running a Github-style page loader across the top and deferring loading using skeleton UI for more backend heavy sections and other content that's below the fold. I did get a chance to try this approach in RRv4 a month or so ago but I'm fairly certain it's not possible, hence posts like this. As @tmeasday mentioned, routes aren't "available" as they were in v3, so traversing the tree is a much different task.
A factored out server renderer would be sick! Looking forward to it ππΌ
@helfer I'll work on abstracting the server renderer to a new package after stabilizing tests!
I could be wrong, so forgive me if l am.
But with React-Apollo and RR4, it works perfectly when using the StaticRouter, could we therefore make use, after every location change, to sub-render the whole app using a StaticRouter then renderToString, then pass the State to the current client and then transition the UI?
I know this sounds really hacky, but my thinking is, if we can do it on the server side, i find it hard to believe its impossible to do on the client
Edit helpful rr4 package which introduces static routing: https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config
This issue has been automatically marked as stale becuase it has not had recent activity. It will be closed if not further activity occurs. Thank you for your contributions to React Apollo!
This issue has been automatically labled because it has not had recent activity. If you have not received a response from anyone, please mention the repository maintainer (most likely @jbaxleyiii). It will be closed if no further activity occurs. Thank you for your contributions to React Apollo!
This issue has been automatically closed because it has not had recent activity after being marked as no recent activyt. If you belive this issue is still a problem or should be reopened, please reopen it! Thank you for your contributions to React Apollo!
Just checking in on this. Is there another issue or established best practice (in the docs that I've missed perhaps) that follows to solve prefetching using Apollo (e.g., inMemoryCache)? I'm particularly interested in 'lazy' prefetching, i.e., only prefetch the part of the state needed to render the linked page(s) - with an async data request - when a user navigates to a page with those links. I plan on using Apollo for everything (e.g., sans Redux). I'm not wed to React Router if another (potential) router would be better for this.
react-router v6 is coming so it'd be good to "refresh" this issue a bit. Does someone have any thoughts about hooks, suspense, concurrent mode and basically the future of apollo + react-router and a way to preload/prefetch data for a route before visiting that route?
Most helpful comment
I think it's part of a pretty critical feature of React + Apollo that I'd like to invest in, so it would be helpful if it were under the same GitHub org as
react-apollo. That would definitely be my preference but I'm open to other ideas as well.