vuejs / apollo

🚀 Apollo/GraphQL integration for VueJS

Home Page:http://apollo.vuejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setup Apollo for a Vue+SSR app

jarkt opened this issue · comments

I really struggle with the Apollo Setup in a Vue SSR app.
The documentation, also the inofficial, is incompleted or outdated.
I wonder about the fact, that I need to pass the client in two ways. This is my code to create the app (shortened):

import {ApolloClients} from "@vue/apollo-composable";
import {createApolloProvider} from "@vue/apollo-option";

const apolloClients = {
  default: apolloClient
}

const app = createSSRApp({
  setup () {
    provide(ApolloClients, apolloClients)
  },
  render: () => (
    <Suspense>
      <Main/>
    </Suspense>
  )
})

const apolloProvider = createApolloProvider({
  defaultClient: apolloClients.default,
  defaultOptions: {
    $query: {
      fetchPolicy: 'no-cache'
    }
  }
})
app.use(apolloProvider)

The provide seems to be needed to find the client in the components setup functions.
And without the app.use, SSR would not work (I believe the data are loading, but nobody waits for it). But both in combination seems not to be documented.

Okay, it runs so far. But there is a very strange thing:
In the doc (https://v4.apollo.vuejs.org/guide-advanced/ssr.html) it seems that I should create an apollo client while application startup time. But the apollo doc (https://www.apollographql.com/docs/react/performance/server-side-rendering/#example) says:

It's important to create an entirely new instance of Apollo Client for each request. Otherwise, your response to a request might include sensitive cached query results from a previous request.

Why the Vue-Apollo doc doesn't reflect it?

Another issue is the SSR. Maybe coupled with the previous question. If I set the fetchPolicy to "no-cache" for test and dev reasons, there appears a hydration mismatch problem. Although I give the "state" to the client. The initial HTML is is generated correct, and the client is also able to generate the same result, but while the hydration time, the data are not available and so is the result another. (That means the page is rendered correctly, than content disappears and reappears immediately after client has loaded the content.)

const state = ApolloSSR.exportStates(apolloClients)
<script>
    {{ state }}
</script>

The "state" seems to be the "cache". The documentation uses renderState in the template. (https://v4.apollo.vuejs.org/guide-advanced/ssr.html) But I have not found that function.

My main problem was, that I have not created a fresh vue app instance at every request.

@jarkt, could you tell us in more detail how you managed to solve this issue?

To be honest, I can't say much more. It was just irritating because my code wasn't creating a new Vue app instance on every request and I didn't question it.
In truth the vue-apollo docs saying nothing about that and I was just thinking I have to create the client at startup time.
And because the apollo docs saying I have to create a new instance with each request I was confused.

Now I create the vue app and also the apollo client on every request and this had solved also the state issue.

@jarkt, isn't the Vue app and Apollo client automatically created on every request? How did you change this behavior? Perhaps you can demonstrate the code? I just had a similar problem but never found a solution

Don't sure what you mean with "automatically". Maybe in Nuxt, yes. But I have a custom setup with Vite+Vue and so I have to care about that for myself.
There is an express app on the outside which handles the requests. There is a catch-all-route and the controller then creates the vue and apollo instances.

What does your setup look like?

@jarkt, i have something similar. On every request, Express calls the render function:

server.js

export async function createServer() {
  
  const app = express();
  const router = express.Router();

  app.use(express.static("dist/client", { index: false }));

  router.get("/*", async (req, res, next) => {
    try {
      const template = await fs.promises.readFile(
        resolve("./dist/client/index.html"),
        "utf-8"
      );

      const render = await (
        await import("./dist/server/entry-server.js")
      ).render;

      const { appHtml, apolloState } = await render(req);

      const html = template
        .replace("<!--apollo-state-->", apolloState)
        .replace("<!--app-html-->", appHtml);

      res.status(200).set({ "Content-Type": "text/html" }).end(html);
    } catch (e) {
      next(e);
    }
  });

  app.use("/", router);

  return { app };
}

export const run = (app) => {
  app.listen(3000, () => {
    console.log("http://localhost:3000");
  });
};

createServer().then(({ app }) => run(app))

src/entry-server.js

import { renderToString } from "vue/server-renderer";
import { createSSRApp, provide, h } from "vue";
import App from "@/App.vue";
import router from "@/router";

import { ApolloClients } from "@vue/apollo-composable";
import { defaultApolloClient } from "@/apollo";
import { exportStates } from "@vue/apollo-ssr";

export async function render(req = null) {
  const app = createSSRApp({
    setup() {
      provide(ApolloClients, {
        default: defaultApolloClient
      });
    },
    render: () => h(App),
  });

  app.use(router);

  await router.push(req.url);
  await router.isReady();

  const ctx = {};
  const appHtml = await renderToString(app, ctx);

  const apolloState = exportStates(
    { defaultApolloClient }
  );

  return {
    appHtml,
    apolloState,
  };
}

As far as I understand, a Vue and Vue Apollo will be created on every request. Is not it so? However, apolloState is not reset after page reload

Please try:

  const apolloState = exportStates(
    { default: defaultApolloClient }
  );

(Further up you used a different key.)

@jarkt, thanks for the hint, but it didn't give any results

Simple example:

  1. I open page 1 in browser 1, where there are no GraphQL requests:
    <script>window.__APOLLO_STATE__ = {"default":{}};</script> // Apollo State is not present

  2. I open page 2 in browser 2, where there are GraphQL queries:
    <script>window.__APOLLO_STATE__ = {"default":{ ... }};</script> // Apollo State is present

  3. I open page 1 in browser 1, where there are no GraphQL requests:
    <script>window.__APOLLO_STATE__ = {"default":{ ... }};</script> // Apollo State is present

That is, it turns out that a common state is used for all users

Ah yes, of course. JavaScript modules are evaluated only once. apollo.js exports the client, but it's always the same. You have to use a function to create one.

@jarkt, something like that?

  const app = createSSRApp({
    setup() {
      provide(ApolloClients, {
        default: () => defaultApolloClient
      });
    },
    render: () => h(App),
  });

This does not work, the page containing GraphQL queries does not open, and there are errors in the log

node-1   | TypeError: client.watchQuery is not a function
node-1   |     at start (file:///srv/app/node_modules/@vue/apollo-composable/dist/index.mjs:309:26)
node-1   |     at useQueryImpl (file:///srv/app/node_modules/@vue/apollo-composable/dist/index.mjs:567:5)
node-1   |     at useQuery (file:///srv/app/node_modules/@vue/apollo-composable/dist/index.mjs:237:10)
node-1   |     at setup (file:///srv/app/dist/server/entry-server.js:792:9)
node-1   |     at _sfc_main.setup (file:///srv/app/dist/server/entry-server.js:1128:23)
node-1   |     at callWithErrorHandling (/srv/app/node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js:18:18)
node-1   |     at setupStatefulComponent (/srv/app/node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js:5822:25)
node-1   |     at setupComponent (/srv/app/node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js:5809:36)
node-1   |     at renderComponentVNode (/srv/app/node_modules/@vue/server-renderer/dist/server-renderer.cjs.prod.js:354:15)
node-1   |     at renderVNode (/srv/app/node_modules/@vue/server-renderer/dist/server-renderer.cjs.prod.js:483:14)

P.S. By the way, this option doesn’t work either, when you try to open a page, a request to the server always occurs, since Apollo cannot detect state

   const apolloState = exportStates(
     { default: defaultApolloClient }
   );

And if you do it the way it was before, it works, but a common state is used

  const apolloState = exportStates(
    { defaultApolloClient }
  );

No. You're importing the apollo client in this line: import { defaultApolloClient } from "@/apollo";
That means you have an apollo.js which exports a client. But since the apollo.js was only evaluated once, you will get always the same client. The apollo.js must export a function to create the client.
That would end in:

  import { getDefaultApolloClient } from "@/apollo";
  ...
  const app = createSSRApp({
    setup() {
      provide(ApolloClients, {
        default: getDefaultApolloClient()
      });
    },
    render: () => h(App),
  });

And the other one, I'm very sure, that

   const apolloState = exportStates(
     { default: defaultApolloClient }
   );

is what you want, because you naming is not consistently otherwise. Don't know what the consequences are... I would not expect that it works.

@jarkt, I tried exporting as a function:

export function defaultApolloClient() {
  return new ApolloClient({
  link: defaultLink,
  cache: defaultApolloClientCache,
  ssrMode: import.meta.env.SSR
  });
}
  const app = createSSRApp({
    setup() {
      provide(ApolloClients, {
        default: defaultApolloClient()
      });
    },
    render: () => h(App),
  });

This did not give any results, the test fails (see #1516 (comment))

Are you sure that everything is working correctly for you?

P.S. The key named default is not important in this case, since it is used to restore the cache from the APOLLO_STATE variable, something like this:

const defaultApolloClientCache = new InMemoryCache();

if (!import.meta.env.SSR) {
  if (typeof window !== "undefined") {
    if (typeof window.__APOLLO_STATE__ !== "undefined") {
      defaultApolloClientCache.restore(
        window.__APOLLO_STATE__.defaultApolloClient
      );
    }
  }
}

As you can see, I'm using the defaultApolloClient key, not default

My code is working fine.
Your naming is still inconsistent. "default" at the one "defaultApolloClient" at the other side...
Otherwise it's weird, that you get no result with the first request, but with the following.
I also noticed, that there is no app.use(apolloProvider) in your code.
That's maybe the reason for the behaviour.

Your naming is still inconsistent. "default" at the one "defaultApolloClient" at the other side...

It doesn't matter in this case, as I wrote above, this name is used to restore the cache

Otherwise it's weird, that you get no result with the first request, but with the following.

Nothing weird, I'm using a key named defaultApolloClient, not default (see #1516 (comment))

I also noticed, that there is no app.use(apolloProvider) in your code.

I'm not using the apolloProvider that is part of the @vue/apollo-option package because I'm using the Composition API (@vue/apollo-composable)