-
-
Notifications
You must be signed in to change notification settings - Fork 522
-
This is along the same lines are #187, but I believe the plugin's internal API has changed since that issue was opened so the title is outdated. Also possibly related to #441.
I have an SSR app using this plugin alongside the the SSR plugin. I use the asyncData method to resolve non-Apollo async API requests on the server side and also before route components are resolved on the client side. Currently most route changes in the app await the result of the asyncData method before resolving and show a loading indicator before any content is displayed, but routes that use Apollo instead of asyncData resolve immediately with a flash of either empty or outdated content, execute the query and then update.
This plugin currently uses Vue's serverPrefetch
method to resolve Apollo queries on the server, but I am struggling to find a way to ensure queries are executed on the client side before routes are resolved. It may be possible to execute queries manually in the asyncData method, but it seems that approach isn't recommended as you lose a lot of nice features this plugin provides.
Is anyone able to provide a solution for this?
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
Replies: 9 comments
-
@rdunk the way that we handle this is to use vuex for things that are related to routing (permissions, flags, etc) and use apollo more for fetching data in that context. I do agree that it would be great to tie into the vue lifecycles more fully but i think that would be a bigger change.
Beta Was this translation helpful? Give feedback.
All reactions
-
I'm running into an issue, using @nuxtjs/apollo
, that I believe has something to do with this limitation. Please correct me if I'm wrong.
Basically, if I hit the following page directly, SSR will make sure that todos
data is available on page load, so this works fine. However, if I navigate to the todos
page with client-side navigation, todos
will be undefined
on initial page load, and we get Cannot read property 'length' of undefined'
. And of course, the same will happen under SSR if I turn off the prefetch
option.
// pages/todos.vue <template> <section>Todos count: {{ todos.length }}</section> </template> <script> import gql from "graphql-tag"; const TODOS_QUERY = gql` query { todos { id title } } `; export default { apollo: { todos: { query: TODOS_QUERY } } }; </script>
Right now I'm using the Apollo client directly via Nuxt's asyncData
to get around that problem, but it would be nice to keep all the benefits of vue-apollo
.
Beta Was this translation helpful? Give feedback.
All reactions
-
@Merott could you setup an small example demoing this?
Beta Was this translation helpful? Give feedback.
All reactions
-
@Austio Sure. Here it is: https://github.com/Merott/demo-vue-apollo-issue-762
Beta Was this translation helpful? Give feedback.
All reactions
-
@Merott thanks for setting up an example for this. I can't express how much easier it makes it to help. I can see how this would be confusing.
Vue issue
At it's core for vue, this is an issue related to having good defaults for anything that will go in the template and handling state as it changes. You can reproduce the error you see by deleting everything in the "script" section of "todos.vue" and then doing a client side navigation.
Both scenarios look like this to vue
- vue gets to $mount, calls render
- render is called, there is no value for the property todos on this (so undefined) and calling length on undefined errors
More specifically on your example that fails for todos
- vue-apollo is created on the vue instance, looks for data, does not see it and dispatches a request to apollo to get the data
- apollo async grabs data
- meanwhile, vue continues with the mount process, which gets to the render error noted above
Why does this work sometimes
This works on SSR because
- on Server: apollo uses serverPrefetch to request data (vue awaits this to resolve before rendering)
- that data is sent to and hydrated into the client
- the data is available on the client when navigating
This works with asyncData because
- asyncData will not mount until the promise using apollo resolves, so mount is called after data is there
The core issue for Apollo to me is that there isn't a tight integration of the "interface" between how things will work on the server vs will work on the client. However, that is a really big ask because it would make a lot of assumptions. Most of this will be much better once apollo-composable is the way things work because there will be way less magic.
How to fix
So the issue with this is related to having good default values for loading and ensuring things are present before they are rendered. Here is a diff that works on client and server. The only real technical recommendation i have is to make sure you init data with the values you expect to be there from apollo
diff --git a/pages/todos.vue b/pages/todos.vue
index cd47fed..53b7763 100644
--- a/pages/todos.vue
+++ b/pages/todos.vue
@@ -1,13 +1,23 @@
<template>
- <section>Todos count: {{ todos.length }}</section>
+ <div>
+ <section v-if="loading === 1">Loading stuff</section>
+ <section v-if="todos !== null">Todos count: {{ todos.length }}</section>
+ </div>
</template>
<script>
import gql from "graphql-tag";
export default {
+ data() {
+ return {
+ loading: 0,
+ todos: null,
+ }
+ },
apollo: {
todos: {
+ loadingKey: 'loading',
query: gql`
query {
todos {
TO get a better grasp on this, you can add a watch on todos
and loading
to watch their value change
+ watch: {
+ loading(n, o) {
+ debugger;
+ },
+ todos(n,o){
+ debugger;
+ },
+ },
Beta Was this translation helpful? Give feedback.
All reactions
-
@Merott the pattern my team has adopted is to use asyncData
to prefetch the query rather than the apollo
property:
// pages/todos.vue <template> <section>Todos count: {{ todos.length }}</section> </template> <script> import gql from "graphql-tag"; const TODOS_QUERY = gql` query { todos { id title } } `; export default { data() { return { todos: [], } }, async asycData(context) { const client = context.app.apolloProvider.defaultClient; const response = await client.query(TODOS_QUERY); return { todos: response.data.todos }; } }; </script>
By returning the results from the query, there’s a chance you might duplicate the in-page hydration cache, in which case you could remove the return from asyncData
, add back in the apollo
property with the same query and set its fetch policy to cache only.
// pages/todos.vue <template> <section>Todos count: {{ todos.length }}</section> </template> <script> import gql from "graphql-tag"; const TODOS_QUERY = gql` query { todos { id title } } `; export default { data() { return { todos: [], } }, async asycData(context) { const client = context.app.apolloProvider.defaultClient; await client.query(TODOS_QUERY); } apollo: { todos: { query: TODOS_QUERY, fetchPolicy: ‘cache-only’ } } }; </script>
We’ve had pretty good success with this approach. I would love it if there were a way to do this automatically via Vue Apollo, but I’m not sure that would be practical because it would be difficult to predict which queries should be fetched this way. For example, in complex parts of our application we will bundle up queries that are necessary for determining then a page is "loaded" into our asyncData
query using query fragments and then reuse those fragments in individual components to get the data where it is needed.
Your mileage may vary, but it’s an approach that’s working for us.
Beta Was this translation helpful? Give feedback.
All reactions
-
@Austio thanks for the explanation.
What I was in fact trying to avoid was having to implement a loading
state for my Vue pages. I'd assumed that the prefetch option in vue-apollo
behaves similarly to asyncData
, which is why I thought my implementation would work for both SSR and client-side navigation.
@gwardwell interesting approach, thanks for sharing. I gave that a try in the demo project that I set up, and it seems to work. Will give it a try in my project later to see how it goes. Thanks!
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Can I bump this a bit?
Twould be really helpful to have a simple way, be it a flag or something on a smart query? To integrate more firmly with vue-router (and nuxt's asyncData
where applicable).
Even having a promise on apolloProvider
that resolves when all smart queries have data, (even if it's cached data that will be updated post-navigation) would be a great start!
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
You can work around with a composable:
import type { Ref } from 'vue'; import { watch } from '#imports'; export const useClientPrefetch = async (loading: Ref<boolean> | Array<Ref<boolean>>) => { if (process.client) { if (!Array.isArray(loading)) { loading = [ loading ]; } if (!loading.length) { return; } await new Promise<void>((resolve) => { watch(loading, () => { let loaded = true; for (let i = 0, len = (loading as Array<Ref<boolean>>).length; i < len; i++) { if ((loading as Array<Ref<boolean>>)[ i ].value) { loaded = false; break; } } if (loaded) { resolve(); } }, { immediate: true }); }); } };
In your component:
<script lang="ts"> export default defineComponent({ async setup() { const { result, loading } = useQuery(QueryDocument); await useClientPrefetch(loading); } } </script>
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1