Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Pre-fetch data recursively before a route transition #153

Closed
amannn opened this issue Aug 18, 2016 · 11 comments
Closed

Pre-fetch data recursively before a route transition #153

amannn opened this issue Aug 18, 2016 · 11 comments
Labels

Comments

@amannn
Copy link
Contributor

amannn commented Aug 18, 2016

Congratulations on the release of v0.4! Looks really good to me!

I once commented on an issue of react-router-apollo (which is now a bit outdated) with a description of a scenario that I'd like to implement using react-apollo.

It's actually very similar to when you're on https://github.com/apollostack/react-apollo and click on the Issues tab (and back to Code if you like). The loading indicator tells you that the app is loading and once all data for the target page is loaded, the transition will happen. If the loading fails, I want the user stay on the same page so she can try again by clicking on the same link again.

I'm pretty sure this is already possible with the current state of Apollo, however it get's a bit more challenging when there are nested graphql containers within a route handler that will only render when the parent container got its data and renders.

I've noticed that there's now the getDataFromTree function that can be used to pre-fetch data for a given tree. As far as I understand, this works recursively also for nested containers, right? My goal is that there can be a route transition to a complex page with lots of nested graphql containers without showing a loading spinner once on the target page.

Could this be used for my scenario? I've seen that the method is imported from react-apollo/server, so I wasn't sure if it's a good idea to also do this on the client side? Maybe there are other things to consider as well. It seems like getDataFromTree produces a new store state, whereas in my case I'd like it to act on an existing store.

Thanks for your help!

@jbaxleyiii
Copy link
Contributor

@amannn this is really interesting. I'm not sure the best way to go about doing this. A variation on getDataFromTree may be what you are looking for but I'm not totally sure

@amannn
Copy link
Contributor Author

amannn commented Aug 23, 2016

I got a non-recursive version working with a react-router onEnter hook.

The gist is:

function fetchRouteData(nextState, replace, cb) {
  const props = {params: nextState.params, location: nextState.location};
  const result = this.component.fetchData(props, {client});
  if (result instanceof Promise) result.then(() => { cb(); });
  else cb();
}

That works pretty well so far.

One thing that might could be improved though is that I have to put a check like the following in the render function:

render() {
    if (this.props.data.loading) return null;
}

As far as I understand, while server rendering the getDataFromTree method will try to render my component before it has fetched the query that it depends upon.

Is that correct? If so, does that behaviour really make sense? My usecase is somewhat special but what if that component would render another graphql-wrapped component that's only rendered when the parent component renders with its fetched data? Wouldn't it make sense to wait for the fetchData promise to resolve until the tree is traversed further?

@jbaxleyiii
Copy link
Contributor

@amannn the server will go to the deepest part of the tree it can. Then fetch the data, then pick up there to keep going.

render() {
    if (this.props.data.loading) return null;
}

If you do the above, it will stop there, then pick up rendering down the tree after it gets data

@amannn
Copy link
Contributor Author

amannn commented Aug 23, 2016

Oh, really? That's pretty cool actually.

I just tried it out. If a put console.log above that condition, I can see it printed 3 times. The first one is to go as deep as it can (without data), the second one is with data to look for new queries down the tree and the third one is from my renderToString call I suppose.

Really cool anyway!

@stubailo
Copy link
Contributor

I think pre-caching data based on traversing the view hierarchy is a Really Cool Idea. @amannn if you get this working, I'd love to learn more about how we can make it easier, because that would be a seriously killer feature.

@amannn
Copy link
Contributor Author

amannn commented Aug 24, 2016

I did an experiment with running getDataFromTree on the client side. Seems to work pretty good. I've noticed there's a 2nd parameter to that function where a pre-existing context can be supplied.

In a very simple case where you have a graphql-wrapped component, it's possible to prefetch all of its data with something like:

const context = {client};
getDataFromTree(component, context).then(() => {
  console.log('all data down the tree pre-fetched');
});

That's pretty simple if the apollo client is the only thing that needs to be present on the context.

However in my scenario it's a bit more difficult. I also need router from react-router and intl from react-intl on the context, as my components depend on them for rendering. It seems like it's not possible to get access to a pre-existing context in the onEnter hook of react-router.

So the options for my use case are

  • a) re-creating a context with all *Provider wrappers every time in the onEnter hook (bloated and error-prone)
  • b) supplying all the instances for context with an object like I did in the snippet above (pretty hackish depending on what context features an app uses, but possible).
  • c) stick with non-recursive data fetching for route handlers (the snippet I posted above). Maybe use fragments for nested components?

So I think the bottom line for my use case is that it's possible, but pretty hackish. However this is more related to the react-router API (or my weird use case 😀) – I think from the Apollo side there's not much to improve.

It's also worth noting that the maintainers of react-router think it's bad practice to do data fetching in onEnter. On the other side people are doing it anyway. I think it's ok, as long as you're showing a loading indicator.

The only thing that could be improved on the Apollo side, is that an app that imports getDataFromTree also imports the complete react-dom/server package which is probably not what you want to happen on the client side. Apart from that I think apollo-client is already capable of pre-caching data for view hierarchies, independently of server side rendering.

@amannn amannn closed this as completed Aug 24, 2016
@stubailo
Copy link
Contributor

It would be really cool to get a blog post going about this!

@gajus
Copy link

gajus commented Sep 13, 2016

I am struggling with server-side rendering when using an existing redux store.

Here is my route handler:

const handleRoute = (res, routes, renderProps) => {
  const client = new ApolloClient({
    ssrMode: true,
    networkInterface: createNetworkInterface('http://127.0.0.1:8003/')
  });

  const store = createStore(undefined, client);

  const app = <ApolloProvider client={client} store={store} immutable={true}>
    <RouterContext {...renderProps} />
  </ApolloProvider>;

  const test = {client};

  getDataFromTree(app, test)
    .then((context) => {
      console.log('test.client.store.getState().apollo', test.client.store.getState().apollo);
      console.log("context.store.getState().get('apollo')", context.store.getState().get('apollo'));
      console.log("store.getState().get('apollo')", store.getState().get('apollo'));
    });
};

Here is the output:

test.client.store.getState().apollo { queries:
   { '0':
      { queryString: 'query ($blogId: ID!) {\n  blog(id: $blogId) {\n    posts {\n      id\n      body\n      createdAt\n      name\n    }\n  }\n}\n',
        query: [Object],
        minimizedQueryString: 'query ($blogId: ID!) {\n  blog(id: $blogId) {\n    posts {\n      id\n      body\n      createdAt\n      name\n    }\n  }\n}\n',
        minimizedQuery: [Object],
        variables: [Object],
        loading: false,
        networkError: null,
        graphQLErrors: null,
        forceFetch: false,
        returnPartialData: false,
        lastRequestId: 1,
        fragmentMap: {} },
     '2':
      { queryString: 'query ($blogId: ID!) {\n  blog(id: $blogId) {\n    posts {\n      id\n      body\n      createdAt\n      name\n    }\n  }\n}\n',
        query: [Object],
        minimizedQueryString: 'query ($blogId: ID!) {\n  blog(id: $blogId) {\n    posts {\n      id\n      body\n      createdAt\n      name\n    }\n  }\n}\n',
        minimizedQuery: [Object],
        variables: [Object],
        loading: false,
        networkError: null,
        graphQLErrors: null,
        forceFetch: false,
        returnPartialData: false,
        lastRequestId: 3,
        fragmentMap: {} } },
  mutations: {},
  data:
   { '$ROOT_QUERY.blog({"id":"10"}).posts.0':
      { id: '3',
        body: 'a',
        createdAt: 'Wed Aug 31 2016 12:20:09 GMT+0100 (BST)',
        name: 'a' },
     '$ROOT_QUERY.blog({"id":"10"}).posts.1':
      { id: '4',
        body: 'b',
        createdAt: 'Wed Aug 31 2016 12:20:13 GMT+0100 (BST)',
        name: 'b' },
     ROOT_QUERY: { 'blog({"id":"10"})': [Object] },
     '$ROOT_QUERY.blog({"id":"10"})': { posts: [Object] } },
  optimistic: [] }

context.store.getState().get('apollo') { queries: {}, mutations: {}, data: {}, optimistic: [] }

store.getState().get('apollo') { queries: {}, mutations: {}, data: {}, optimistic: [] }

test.client.store.getState().apollo is the expected output. But this state is not registered with either context returned from getDataFromTree or the store passed to the ApolloProvider.

What am I missing?

@stubailo
Copy link
Contributor

Can you open a separate issue on react-Apollo about this please?

@estrattonbailey
Copy link

Thanks for this thread @amannn, and thanks for all your work @stubailo, Apollo is 💯

I've recently been investigating the idea of prefetching views as well and have a tentative solution that relies on react-router's renderProps passed to a router middleware: apollo-prefetch. It's rough and untested in production, still trying to suss out whether it's a true solution to this problem, but it works well with my current local setup and I hope to continue development to a production level.

Not sure if this helps, just hoping to get some experienced eyes on it 😁 would love any feedback or questions you have.

@stubailo
Copy link
Contributor

stubailo commented Jan 9, 2017

I think that's a good start! Perhaps we could use some of the machinery from server side rendering to make this happen? It already crawls the component tree for data to fetch: https://github.com/apollostack/react-apollo/blob/6743af261f9cc8ebb840fa163f6e1e6711bc5f3f/src/server.ts#L97

Also, can you open a new issue about it? This one is somewhat old.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants