Skip to content
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

Server Side Queries #14

Closed
marutypes opened this issue Dec 5, 2016 · 23 comments
Closed

Server Side Queries #14

marutypes opened this issue Dec 5, 2016 · 23 comments

Comments

@marutypes
Copy link

The Struggle

Hello, I'm trying to get an isomorphic vue vue-router vue-apollo application running and I've run into some issue.

vue-apollo does not run on the server by default. To try to get around this I first tried to manually run queries before rendering the application on the server by pullinf from router.getMatchedComponents. This lead me to my next issue.

vue-router's router.getMatchedComponents yields only the component definitions, not instances. This means we cannot run the queries in the context of the running state of the application. This lead me to try to run the queries from within component lifecycle hooks.

Unfortunately component lifecycle hooks cannot be asynchronous. As a result the queries are run after the application has already been rendered and sent down the wire.

I'm more or less out of ideas now, and I'll probably just fall back to using apollo client directly with vuex, which is pretty terrible.

TLDR

It seems like this plugin has no way to operate in conjunction with server side rendering. I'd love to be wrong however. Do you have any ideas how it could be done?

@Akryum
Copy link
Member

Akryum commented Dec 5, 2016

apollo-client needs fetch to run. Since it is only available in a browser, try installing node-fetch and exposing it globally:

import fetch from 'fetch'
global.fetch = fetch

See #13

@Akryum
Copy link
Member

Akryum commented Dec 5, 2016

More could be done to improve compatibility with the server, if you have any idea please don't hesitate to share them.

@marutypes
Copy link
Author

marutypes commented Dec 5, 2016

I already am using fetch on the server. My problem is that as it is you can't actually have your components defer rendering until they have data, so you end up with no data if your render is faster than the fetch, or alternatively trying to run the queries manually without component instances.

I have something sort of kind of working now using the following code, but it's limited to queries that don't have any dynamic variables (since we don't have a this context when we try to run them)

// server-entry.js
// Apollo in this context is a wrapper I made around apollo client that sets some api credentials for my app

export default (context) => {
  // set session to our passed in express session data
  if (context.session) {
    store.commit('setSession', context.session);

    const {accessToken, shop} = store.state.session;

    console.log(context);

    Vue.use(VueApollo, {
      apolloClient: Apollo.init(accessToken).client(),
      ssrMode: true,
    });
  }

  // set router's location
  router.push(context.url);

return Promise
    .all(router.getMatchedComponents().map((component) => {
      /* `component` in this function is the definition, not an instance.  
      Any methods we run on it (for example, `variables` will not have access to 
      real props or anything like that*/
      if (component.apollo && Apollo.client()) {
        const dataDependencies = component.apollo;
        /* if we have a vue-apollo field manually run the query. 
        We don't have a this context so we have to do it pretty naively */
        const queries = Object.keys(dataDependencies)
          .map((key, index) => {
            const definition = dataDependencies[key];
            return Apollo.client().query({query: definition.query || definition}); // this could obviously be better but it works for now
          });

        return Promise.all(queries);
      }
      return null;
    }))
    .then(() => {
      console.log(`data pre-fetch: ${Date.now() - startTime}ms`);
      // the request handler will inline the state in the HTML response.
      context.initialState = store.state;
      if (Apollo.client()) {
        context.apolloState = Apollo.client()
          .store
          .getState()
          .apollo;
      }
      return app;
    });

Then we inline context.apolloState inside of our render function and use it to hydrate initialState on the client.

This works for queries that only depend on the route and store and other top level app information. If there is any way to perform asynchronous operations inside a lifecycle hook or otherwise defer component renderering from a plugin you could potentially check Vue.$isServer in a hook and have DollarApollo do it's magic there, but I've yet to turn up anything like that.

If I'm overthinking this and you have a working ssr example please let me know.

@matteodem
Copy link

Ran into the same problem. Isn't the easiest solution to not load any data from graphql on the server and make sure inside the library that both server and client render the same html?

If the loadingKey property is consistent this issue is pretty much resolved for me.

@marutypes
Copy link
Author

marutypes commented Dec 6, 2016

Ran into the same problem. Isn't the easiest solution to not load any data from graphql on the server

Unfortunately, that's an unacceptable solution for our app. If that's the best we can do I'll probably have to recommend we use react instead.

For now I'm using my solution above where I manually run what queries I can without access to the live instances, we'll see how far that gets me :/

@matteodem
Copy link

Okay, yeah makes sense. I'm not sure how much of this logic should be part of this library, because server side vuejs still requires quite a lot of boilerplate code and it doesn't seem like there's an "official way" to do things (see hackernews vue clone).

Maybe it makes sense to kickstart a discussion on apollo-client? Unless there's already a solution on apollo level that I don't know about.

@Akryum
Copy link
Member

Akryum commented Dec 7, 2016

I'm exploring solutions for vue-apollo that would make server-side querying easy. If you have suggestions, please feel free to share!

@matteodem
Copy link

matteodem commented Dec 7, 2016

wouldn't it be cool to have something like:

export default {
  ...
  preFetch(store) {
    return this.$apollo.initialize();
  }
  ...
};

Which would return a promise that resolves after a client.query and succesful instantiation of the data? In the hackernews example they use the vuex store to instantiate the client with the data. But like shown in the example by @TheMallen we could also have some custom serialized state.

Btw ignore the wording, should be called something more descriptive (instead of initialize)

@marutypes
Copy link
Author

I'm basically doing that manually atm, and it mostly works, as long as my queries only care about route parameters :/

@Akryum
Copy link
Member

Akryum commented Dec 7, 2016

Maybe I could fake this and the data hook, sort of.

@matteodem
Copy link

@TheMallen can you show us the code where you use apolloState on the client-side?

@Akryum
Copy link
Member

Akryum commented Dec 19, 2016

What do you think of this?

export function prefetch(router) {
  return Promise.all(router.getMatchedComponents().map(component => component.$apollo.promise.allQueries))
}

With $apollo.promise.allQueries being a promise resolved when all the queries are resolved.

@Akryum
Copy link
Member

Akryum commented Dec 19, 2016

Another proposition (for instance-less use-case):

export default {
  apollo: {
    someData: {
      query: ...,
      variables () {
        ...
      },
      prefetchVariables: context => {
        ...
      },
    }
  }
}


// vue-apollo API

export function prefetchQueries (componentDefinition, context) {
  return Promise.all(...)
}

export function prefetchAll (router, context) {
  return Promise.all(router.getMatchedComponents().map(component => prefetchQueries(component, context)))
}

@matteodem
Copy link

Looks cool. Would apollo take over the serialization process or how would that work? As in how would we be able to instantiate the vue apollo instance with data on the client?

@Akryum
Copy link
Member

Akryum commented Dec 20, 2016

It may not do that in the first iteration, but that would be nice to have indeed. 😄

@matteodem
Copy link

I think having that function would already greatly simplify the server side pre fetching. Any way of helping you with this?

@Akryum
Copy link
Member

Akryum commented Apr 2, 2017

We can start playing with SSR! v2.1.0-beta.1

@Akryum
Copy link
Member

Akryum commented Apr 4, 2017

Here are some brainstorming about how it may look like (I didn't test it yet, but if someone has time, he can 😄).

@Akryum
Copy link
Member

Akryum commented Apr 26, 2017

I think we will need to walk the possible tree of components like what react-apollo does.

@matteodem
Copy link

I'll definitely check out the SSR this weekend.

@Akryum
Copy link
Member

Akryum commented Apr 30, 2017

Mostly final SSR API has landed in beta.8!

@Akryum Akryum closed this as completed Apr 30, 2017
@dohomi
Copy link
Contributor

dohomi commented May 1, 2017

@Akryum great news. Was looking for the beta.8 release, did you publish it already? The improvements seem to make a working example now for nuxt as far I could read?

@dohomi
Copy link
Contributor

dohomi commented May 1, 2017

@Akryum just found the npm release, I've been confused because this repo does not show a release note for beta.8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

No branches or pull requests

4 participants